MokaByte 74- Maggio 2003 
Multi Media API
Le api J2ME per la gestione dei contenuti multimediali su dispositivi embedded
di
Marco Tomà
I telefoni cellulari in commercio sono sempre più performanti: aumenta, infatti, sia il quantitativo di memoria sia le capacità di calcolo dei processori installati. A fronte di tale incremento di prestazioni era ragionevole aspettarsi un adeguamento del software a tali cambiamenti.

Introduzione
Solo alcuni mesi fa parlare della possibilità di vedere, sul proprio cellulare un video, era considerata una cosa fantascientifica, farlo poi utilizzando java come linguaggio di programmazione sembrava davvero "irrealizzabile". Ma, la ram aumenta, i processori corrono sempre più richiedendo sempre meno energia, dunque eccoci qua a parlare di MultiMedia Api (MMA) un insieme di librerie java per la gestione di contenuti multimediali che si basa su CLDC/MIDP.
Tutto ciò che verrà trattato in questo articolo riguarda la Reference Implementation di Sun (disponibile per piattaforma Window 2000). Le cose non cambiano, come java ci ha abituato, se cambia la piattaforma. Attualmente però, mentre sono molti i dispositivi che supportano CLDC/MIDP, le MMA sono supportate solo da un dispositivo reale (già disponibile sul mercato): si tratta del Nokia 3650, un tri-band GSM (900, 1800, 1900) che supporta inoltre le WMA (Wireless Messaging Api) di cui abbiamo discusso nel numero di ottobre di MokaByte.
Al di là delle varie implementazioni, il risultato importante è quello di poter disporre di un unico "strumento" per la gestione del "multimediale", garantendo così la portabilità del codice. E' vero, infatti, che molti dispositivi possiedono nel proprio "pacchetto software" players e applicazioni per questo scopo ma, oltre risultare inaccessibili da java, potrebbero essere
una parte integrata (ad esempio un visualizzatore di video mpeg) di una applicazione più complessa. In quest'ultimo caso, infatti, ci si troverebbe a doversi interfacciare con software proprietari molto differenti tra loro. Si pensi ad esempio alla possibilità di visualizzare sullo schermo del dispositivo le immagini riprese da un sistema di telecamere di sicurezza. In questo caso, si potrebbe desiderare che l'applicazione non si limiti al solo rendering del video ma, che dia anche la possibilità di commutare tra le varie telecamere del sistema e di gestirne funzionalità quali lo zoom e il brandeggio. Lasciando da parte i contesti applicati pratici ed entriamo nel dettaglio delle MMA.

 

Controllable e Control
Tutta la "filosofia" MMA ruota intorno al concetto di "controllato e controllo" espressi in java dalle interfacce Controllable e Control. Control rappresenta il controllo che "può essere esercitato" su di un oggetto controllabile (che estende Controllable).
Ogni oggetto controllabile deve poter fornire informazioni sul controllo cui è soggetto, per questo l'interfaccia Controllable possiede due metodi:

public Control getControl( String controlType )
public Control[] getControls( )

il primo permette di ottenere un controllo sull'oggetto controllabile specificandone il nome, il metodo tornerà null se il controllo non esiste. Il secondo metodo ritorna l'elenco dei possibili controlli dell'oggetto Controllable.
Le MMA prevedono vari tipi di controlli, nella figura 1 ne riportiamo l'elenco:




Figura 1 - I controlli previsti dalle MMA

Prima di entrare nei dettagli di ogni singolo controllo, vediamo il più importante degli oggetti controllabili il player.

 

Il Player
Il player (javax.microedition.media) si occupa del rendering di contenuti multimediali, ed estendendo l'interfaccia Controllable può essere controllato "sotto molti punti di vista" che variano in base al tipo di player che si intende realizzare. Esso rappresenta il cuore di tutta l'architettura MMA, fornisce i metodi per il rendering delle informazioni, per la gestione del proprio ciclo di vita, per la sincronizzazione con eventuali altri players e per la gestione dei dispositivi necessari (es. display, altoparlante, ecc).
Come visibile in figura 2 un player può trovarsi in diversi stati:



Figura 2 - Ciclo di vita di un Player

Lo stato di partenza è UNREALIZED: il player non esiste ancora occorre realizzarlo con una chiamata a:

public void realize() throws MediaException


questo porta il player allo stato REALIZED.
In questo stato il player riceve le informazioni necessarie per il rendering dei contenuti multimediali, può leggere file o stabilire una connessione con il server che contiene i contenuti.
Un player non può tornare nello stato UNREALIZED a meno che non venga invocato il metodo:

public void deallocate() throws MediaException

prima di una chiamata a realize() (percorso tratteggiato in figura 2).

Una volta realizzato, il player non è ancora pronto all'utilizzo, occorre ancora un "avanzamento di stato" che si ottiene con la chiamata a:

public void prefetch() throws MediaException

questa operazione permette al player di prendere possesso delle risorse di cui necessita e di eseguire tutte le operazioni di inizializzazione.
Nello stato PREFETECH il player è finalmente pronto all'utilizzo:
lo si può far partire con il metodo:

public void start() throws MediaException

che lo porta nell'omonimo stato (START), quello di esecuzione, oppure si può tornare allo stato precedente (REALIZED) con una chiamata a deallocate().
Durante la fase di START possono verificarsi due diverse situazioni:
una chiamata a:

public void stop() throws MediaException

lo riporta nello stato PREFETECH, mentre invocando:

public void close() throws MediaException

il player rilascia tutte le risorse a sua disposizione si pone nello stato CLOSED.

E' possibile monitorare lo stato di un player ricorrendo al metodo:

public int getState() throws MediaException

che ritorna appunto lo stato in cui si trova il player al momento della chiamata e può assumere uno dei valori costanti appena visti ( UNREALIZED, REALIZED, PREFETCHED, STARTED, CLOSED ).

Altri importanti parametri riguardanti il player possono essere ottenuti e/o settati con i metodi:

public TimeBase getTimeBase()
public void setTimeBase(TimeBase timeBase)
public void setLoopCount(int count)

I primi due permettono rispettivamente di ottenere e settare il TimeBase, cioè la base dei tempi (che deve essere assolutamente costante nel tempo) utilizzata dal player per la sincronizzazione durante la riproduzione dei contenuti. TimeBase dispone di un unico metodo:

public long getTime()

il valore di tipo long ritornato rappresenta il tempo di sincronizzazione espresso in microsecondi.
Per tornare al valore di default, che dipende dalle varie implementazioni, si può richiamare setTimeBase passando null come parametro.

Il metodo setLoopCount, invece, permette di settare il numero di volte che un determinato contenuto verrà riprodotto dal player. Il valore di default è 1, ma può assumere un qualunque intero maggiore di 0, mentre il valore -1 indica una riproduzione a ciclo infinito.

Vediamo ora alcuni metodi del player maggiormente legati al contenuto da renderizzare, si tratta dei metodi:

public long getMediaTime()

ritorna il "puntatore" al tempo corrente relativo al contenuto multimediale durante la riproduzione. Se tale valore (espresso in microsecondi) non può essere determinato verrà ritornata la costante TIME_UNKNOWN.

public long setMediaTime(long now)

permette di spostare "il puntatore" nel tempo corrente relativo al contenuto in riproduzione. Eventuali valori negativi o superiori all'effettiva durata del contenuto comporteranno, rispettivamente, un posizionamento all'inizio e alla fine del contenuto stesso.

public long getDuration()

questo metodo infine è complementare ai due appena visti e permette di ottenere la durata del contenuto che si deve riprodurre. Se la durata non è ottenibile il metodo ritorna la costante TIME_UNKNOWN.

Per concludere la trattazione sui player vediamo come si possono gestire gli eventi che esso è in grado di generare.
La classe player contiene un metodo per agganciare un "ascoltatore" al player stesso:

public void addPlayerListener(PlayerListener playerListener)

ed uno per una eventuale rimozione:

public void removePlayerListener(PlayerListener playerListener)


La classe PlayerListener possiede un unico metodo:

public void playerUpdate(Player player, String event, Object eventData)

che viene richiamato ogniqualvolta il player genera un evento.
I parametri del metodo rappresentano rispettivamente: il player che ha generato l'evento, il tipo di evento (in formato stringa) e un oggetto associato all'evento.

Riportiamo qui di seguito un elenco dei possibili eventi con il relativo significato:

DEVICE_AVAILABLE
DEVICE_UNAVAILABLE


questi due eventi indicano la disponibilità o meno di una risorsa (esclusiva) di cui il player ha bisogno. La ricezione di tali eventi può avvenire se il player si trova nello stato REALIZED. La risorsa potrà essere acquisita dal player con una chiamata a prefetch().

CLOSED
STOPPED
STARTED


Questi eventi sono la conseguenza alla chiamata di close(), stop() e start() rispettivamente.

BUFFERING_STARTED
BUFFERING_STOPPED


segnalano l'entrata e l'uscita del player dalla modalità buffering, cioè dalla fase di caricamento dei contenuti da renderizzare.

DURATION_UPDATED

viene lanciata a seguito dell'aggiornamento della durata del contenuto in esecuzione se questa non è nota a priori. Per alcuni tipi di contenuti multimediali può non essere possibile stabilirne la durata se non dopo averne riprodotto una porzione.

END_OF_MEDIA

viene lanciato quando la riproduzione del contenuto è terminata.

ERROR

lanciato in caso si siano verificati errori.

STOPPED_AT_TIME

viene generato a seguito di una chiamata a setStopTime, un metodo del controllo StopTimeControl di cui parleremo più avanti, ed indica che il player si è fermato come richiesto.

SIZE_CHANGED

indica un cambiamento delle dimensioni del contenuto. La sorgente video e/o le dimensioni del display sono cambiate.

RECORD_STARTED
RECORD_STOPPED


indicano rispettivamente l'inizio e la fine di una fase di registrazione. Se durante tale operazione si verifica un errore verà generato un evento di tipo:

RECORD_ERROR

VOLUME_CHANGED

Generato a seguito di una variazione del livello di volume.

Abbiamo visto che cos'è un player qual è il suo ciclo di vita, come si utilizza e che cosa può fare, ma non abbiamo ancora visto come si crea, cioè come se ne possa ottenere un'istanza.
Per questo motivo ci occupiamo ora della classe Manager presente in javax.microedition.media.

 

La classe Manager: il gestore delle risorse
Manager è il gestore delle risorse del sistema, si occupa della gestione a "basso livello" dei player, nel senso che gestisce l'allocazione e la deallocazione di tutti i dispositivi (display, altoparlante, microfono, fotocamera, ecc.) di cui necessitano i player per il loro funzionamento fornendo inoltre informazioni sull'hardware sottostante.
Attraverso la classe Manager si può ottenere un'istanza di Player con il metodo createPlayer disponibile con tre diverse firme:

public static Player createPlayer(InputStream is, String type) throws IOException, MediaException

public static Player createPlayer(String locator) throws IOException, MediaException

public static Player createPlayer(DataSource source) throws IOException, MediaException

La prima firma del metodo permette la creazione di un player a partire da uno stream di input associato alla risorsa da riprodurre. Il secondo parametro passato (type) rappresenta il "content-type" della risorsa.
Esempi di type possono essere:

audio/x-wav : audio in formato Wave;
audio/basic : audio in formato AU;
audio/mpeg : audio in formato MP3 ;
audio/midi : audio in formato MIDI;
audio/x-tone-seq : sequenza di toni;
video/mpeg : video in formato mpeg;

Con la seconda firma, il player creato è associato al media-locator passato come parametro. La stringa passata, locator appunto, non è una stringa arbitraria ma una URI e come tale ha un ben preciso formato e deve sottostare alle regole previste per le uri (schema, caratteri riservati, caratteri di escape, ecc):

<scheme>:<scheme-specific-part>

dove scheme indica il protocollo utilizzato per la trasmissione dei dati (multimediali in questo caso).
Ad esempio locators validi (Nokia 3650) sono:

device://midi
capture://video
device://tone
http://server/music.mp3

Esistono locator specifici per contenuti multimediali:

· "capture://" device [ "?" params]


permette la "cattura" (registrazione) da sorgenti multimediali (sorgenti audio/video).
Device rappresenta il tipo di dispositivo o il nome del dispositivo stesso (es. "audio", "video", "audio_video", nome device in formato alfanumerico) mentre i parametri possono essere tipici del contenuto oppure possono specifici di un dato apparato (es. rate=16000, encoding=pcm, framepersecond=5, channels=2, bits=8 ecc.).
Una uri completa relativa alla registrazione da una sorgente video potrebbe essere:
"capture://cam0?encoding=rgb&fps=25".

· "rtp://" address [ ":" port ] [ "/" type ]

rpt sta per Real-time Transport Protocol, si tratta di un protocollo di di livello "trasporto", utilizzato per lo streaming di contenuti multimediali. Address e port servono per individuare la sorgente dalla quale effettuare lo streaming, mentre type rappresenta il tipo multimediale (video, audio, ecc.).
Tornando al metodo createPlayer l'ultima firma prevista, prevede il passaggio di un DataSource.
La classe astratta DataSource rappresenta l'astrazione dei protocolli di basso livello che gestiscono il trasferimento dei dati multimediali da renderizzare. DataSource e SourceStream permettono di usufruire di funzionalità particolari non presenti nei normali stream ( InputStream ), ma particolarmente utili nella trasmissione e nella gestione di file multimediali come l'accesso random ai dati (e non solo in modalità "sequenziale") e la possibilità di suddividere un flusso in "porzioni minori". DataSource, implementando l'interfaccia Controllable, può essere controllata attraverso opportuni controlli ottenibili con i metodi getControl e getControls.
Attraverso i metodi:

public abstract void connect()throws java.io.IOException
public abstract void start() throws java.io.IOException
public abstract void stop()
public abstract void disconnect()throws java.io.IOException

è possibile gestire il trasferimento dei contenuti multimediali:

il primo apre una connessione con una sorgente individuata da locator (parametro passato nel costruttore della classe DataSource( String locator) ), per mezzo di start si può avviare il trasferimento dei dati che può essere interrotto con il metodo stop. Infine, con una chiamata a disconnect viene rilasciata la connessione aperta.
Sono inoltre previsti i metodi:

public abstract String getContentType()
public String getLocator()

che ritornano, rispettivamente, il tipo del contenuto multimediale restituito dal DataSource e il locator associato al DataSource stesso.
Il metodo:

public abstract SourceStream[] getStreams()

ritorna un array di SourceStream. Ogni SourceStream nell'array rappresenta una porzione del contenuto totale (un singolo stream). La classe (astratta) SourceStream fornisce i metodi necessari alla manipolazione di questi stream.

public ContentDescriptor getContentDescriptor()

fornisce un descrittore dello stream ( ContentDescriptor ),

public long getContentLength()

ritorna la lunghezza (numero di byte) dello stream,

public int getTransferSize()

ritorna la dimensione dello stream nella sorgente, cioè la dimensione della singola "porzione" rappresentata dallo stream stesso o -1 se questa non è disponibile.

public int getSeekType()

verifica se, e in che modo, lo stream supporta il seek, cioè la possibilità di "spostarsi" all'interno dello stream. I possibili valori di ritorno sono (le costanti intere):

NOT_SEEKABLE: non applicabile,
SEEKABLE_TO_START: ogni operazione di seek è relativa al punto di inizio dello stream,
RANDOM_ACCESSIBLE: lo stream è accessibile in modo random, è quindi possibile posizionarsi in ogni suo punto.

public long seek( int position )

permette il posizionamento nel punto indicato da position, mentre con

public long tell()

è possibile conoscere la posizione corrente nello stream.

Naturalmente non poteva mancare:

public int read(byte[] buffer, int offset, int length) throws IOException

per procedere alla effettiva lettura delle stream. La possibilità di implementare propri DataSource è una caratteristica molto potente delle MMA, in quanto permette il supporto di qualunque protocollo.
La creazione di un player con il metodo Manager.createPlayer( DataSource source), permette infatti di "mascherare" dentro la particolare implementazione di DataSource i dettagli relativi al protocollo. Tale implementazione dovrà fornire al player i metodi appena visti, e ottenere i dati da una implementazione di SourceStream. Quest'ultima sarà il wrapping di una "sorgente" di basso livello (stream, socket o altro) e dovrà fornire al DataSource gli strumenti per il posizionamento e la lettura dei dati ( read(..), seek(..), tell(), etc).
Concludiamo la trattazione della classe Manager, analizzando gli ultimi tre metodi previsti.
Abbiamo detto che Manager costituisce il "gestore" del sistema sottostante ed in quanto tale è l'unico punto da cui è possibile ottenere informazioni riguardanti l'ambiente nel quale le MMA operano.
Il metodo:

public static TimeBase getSystemTimeBase()

ritorna il TimeBase (la base dei tempi), di cui abbiamo già parlato, relativa al sistema.

public String[] getSupportedContentTypes(String protocol)

ritorna una lista di tutti i content-type supportati dal protocollo passato come parametro (protocol).
Se viene passato null la lista conterrà tutti i content-type supportati dall'implementazione.
Ad esempio passando null al metodo getSupportedContentTypes( null ) si ottiene (reference implementation):

audio/x-tone-seq
audio/x-wav
audio/midi
video/mpeg


public String[] getSupportedProtocols(String contentType)

ritorna, invece, una lista dei protocolli previsti per un dato content-type passato come parametro. Se viene passato null, la lista contiene l'elenco di tutti i protocolli supportati dall'implementazione.
Ad esempio passando null nella reference implementation si ottiene:

http
device
capture

 

I controlli
Come visibile in figura 1 le MMA prevedono numerosi controlli che possono essere "applicati" ad un player.

 

FramePositioningControl
Permette il controllo dei frame di video. Ogni frame è numerato, la numerazione parte da 0 che corrisponde all'inizio del video ( cioè quando una eventuale chiamata del metodo getMediaTime() di Player ritorna il valore 0 ), e dipende, ovviamente, dal numero di frame presenti nel contenuto video. Il mapping tra la sequenza di frame e la durata corrispondente è ottenibile con i metodi:

public long mapFrameToTime(int frameNumber)
public int mapTimeToFrame(long mediaTime)


Nel primo caso, dato il numero di un frame viene riportato lo "spostamento temporale", espresso in microsecondi, del frame stesso dall'inizio del video, il secondo metodo fornisce il risultato contrario. Passando, infatti, un valore temporale (sempre relativo all'inizio del video) viene ritornato il frame corrispondente. Entrambi i metodi effettuano esclusivamente le dette conversioni senza operare in alcun modo sul contenuto video. Al contrario i metodi:

public int seek(int frameNumber)
public int skip(int framesToSkip)

permettono di ricercare un determinato frame del video (il primo) e di saltare un certo numero di frame rispetto al frame corrente (il secondo).

 

GUIControl
Permette di interagire con i player utilizzando i dispositivi di i/o della piattaforma sottostante.
Prevede un unico metodo:

public Object initDisplayMode(int mode, Object arg)

che consente di inizializzare il GUIControl. Il parametro mode passato al metodo specifica come il controllo deve essere visualizzato (l'unico valore previsto è USE_GUI_PRIMITIVE).
Il valore ritornato in questo caso è un oggetto GUI di "tipo primitivo", ciò vuol dire che se la piattaforma sottostante supporta AWT l'oggetto sarà di tipo Component, mentre per il MIDP (LCDUI) sarà di tipo Item. Una volta ottenuto un oggetto di questo tipo, lo si può inserire in un container come qualsiasi altro componente (button, textfield, ecc) e visualizzarlo.
Il parametro arg dipende da mode. Nel caso USE_GUI_PRIMITIVE, arg è il nome della classe del componente che si vuole ottenere cioè, "java.awt.Component" nel caso di AWT, "javax.microedition.lcdui.Item" nel caso del midp. Il codice riportato sotto mostra come creare e aggiungere un componente di tipo GUIControl (sia nel MIDP sia in piattaforme che supportano awt).

MIDP
........................
Form form = new Form(title);
.......................
Player player = Manager.createPlayer(...player type...);
player.realize();
GUIControl guiControl = player.getControl("GUIControl");
if(guiControl != null)
{
Item gui = (Item) gc.initDisplayMode(USE_GUI_PRIMITIVE, "javax.microedition.lcdui.Item");
form.append(gui);
}

Display.getDisplay(midlet).setCurrent(form);
player.start();

AWT (es. configurazione CDC)

Player player = Manager.createPlayer(...player type...);
player.realize();
GUIControl guiControl = player.getControl("GUIControl");
if(guiControl != null)
{
Component gui = (Component) gc.initDisplayMode(USE_GUI_PRIMITIVE, "java.awt.Component");
add(gui);
}

Display.getDisplay(midlet).setCurrent(form);
player.start();

 

MetaDataControl
Questo tipo di controllo può essere utilizzato per ottenere metadata riguardanti il contenuto multimediale da riprodurre con il player.
I metodi:

public String[] getKeys()
public String getKeyValue(String key)

permettono di ottenere la lista completa dei campi dei metadata (il primo) e il valore di un determinato campo specificato da key (il secondo).
Le quattro costanti:

AUTHOR_KEY
COPYRIGHT_KEY
DATE_KEY
TITLE_KEY

rappresentano metadata molto comuni che forniscono informazioni circa l'autore, il copyright, la data e il titolo di un dato contenuto.

 

MIDIControl
E' il controllo associato ai dispositivi (sintetizzatori o porte esterne) MIDI.
Il metodi:

public int[] getProgram(int channel)

permette di ottenere il programma (riferito ad un singolo strumento) associato ad uno dei 16 canali disponibili, mentre si può accedere ai programmi relativi ad un banco con il metodo:

public int[] getProgramList(int bank)

il numero massimo di programmi per ogni singolo banco è 128 (numerati da 0 a 127), mentre la variabile bank può assumere un valore compreso tra 0 e 16383.

public String getProgramName(int bank, int prog)

permette di ottenere il nome di un programma dato il banco e la posizione (indice) al suo interno. Con il metodo:

public int[] getBankList(boolean custom)

si può ottenere la lista dei banchi disponibili sia quelli interni che quelli custom, se il parametro è false, mentre solo questi ultimi nel caso sia true.

public String getKeyName(int bank, int prog, int key)

ritorna il nome associato ad una particolare chiave (suono), all'interno di un programma (individuato da prog) in un determinato banco (bank) o null se tale associazione non esiste.
Poiché questi metodi possono non essere supportati da alcune implementazioni di MMA il metodo:

public boolean isBankQuerySupported()

permette di verificare la possibilità di utilizzarli o meno per evitare eccezioni del tipo MediaException. A esempio il seguente codice "esplora" i banchi presenti in un dispositivo:

MIDIControl synthetizer = (MIDIControl)p.getControl("MIDIControl");
if( synthetizer.isBankQuerySupported() )
{
int banks[] = synthetizer.getBankList(false);
for( int i=0; i<banks.length; i++)
{
int programs[] = synthetizer.getProgramList(banks[i]);
for( int x=0; x<programs.length; x++)
{
System.out.println( "BANK " + banks[i] + " - PROGRAM " + programs[x] + " : "+
synthetizer.getProgramName(banks[i],programs[x]));
}
}
}
else
System.out.println(" Bank query not supported ");

Sul Nokia 3650, questo codice produce un output del tipo:

BANK 0 - PROGRAM 0 : Piano
BANK 0 - PROGRAM 1 : Bright Piano
BANK 0 - PROGRAM 2 : Electric Grand
.............................................................
BANK 0 - PROGRAM 125 : Helicopter
BANK 0 - PROGRAM 126 : Applause
BANK 0 - PROGRAM 127 : Gunshot
BANK 1 - PROGRAM 27 : Hi-Q
BANK 1 - PROGRAM 28 : Slap
BANK 1 - PROGRAM 29 : Scratch Push
..............................................................
BANK 1 - PROGRAM 87 : Open Surdo
BANK 1 - PROGRAM 94 : Brush Snare High
BANK 1 - PROGRAM 95 : Brush Snare Low
BANK 2 - PROGRAM 0 : Soft Piano
BANK 2 - PROGRAM 1 : Reflection Piano
BANK 2 - PROGRAM 2 : Flange Piano
...............................................................
BANK 2 - PROGRAM 125 : Cosmic Ray
BANK 2 - PROGRAM 126 : SampHold
BANK 2 - PROGRAM 127 : SampHold 2
BANK 3 - PROGRAM 0 : tablaesque_lo
BANK 3 - PROGRAM 1 : tablaesque_hi
BANK 3 - PROGRAM 2 : nine_inch_kick
..................................................................
BANK 3 - PROGRAM 89 : Gate-tone
BANK 3 - PROGRAM 90 : chem-tone
BANK 3 - PROGRAM 91 : dub_kick

Per potere agire separatamente sul livello del volume di ogni singolo canale, si utilizzano i metodi:

public int getChannelVolume(int channel)
public void setChannelVolume(int channel, int volume)

che permettono, appunto di ottenere il volume di un determinato canale e di settarlo.

public void setProgram(int channel, int bank, int program)

permette di settare un programma in un determinato banco, relativo al canale indicato da channel.

I due metodi

public void shortMidiEvent(int type, int data1, int data2)
public int longMidiEvent(byte[] data, int offset, int length)

infine, servono per inviare eventi ai dispositivi midi.
Nel primo caso i dati passati possono essere 1,2 o 3 byte, il primo parametro (type) è suddiviso in due parti (parte alta e bassa) permettendo quindi di settare il tipo di evento e il canale sul quale lo si vuole inviare. Ad esempio:

shortMidiEvent( NOTE_ON | chNumber, note, velocity );

permette di lanciare l'evento per richiedere l'esecuzione di una data nota, specificando la velocità e il canale di destinazione. Può capitare che alcune implementazioni midi non supportino alcuni tipi di eventi e, non è neppure garantito che un evento sia effettivamente "ascoltato" in questi casi il fallimento del metodo viene superato "in silenzio" senza che venga rilanciata alcuna eccezione.
Il secondo metodo permette l'invio di eventi lunghi ai dispositivi midi (è possibile inviare con questo metodo un insieme di short event). Al metodo devono essere passati l'array contenete i dati relativi agli eventi, il punto di partenza nell'array (offset) e la lunghezza dei dati.

 

PitchControl
E' il controllo relativo al grado di tonalità dell'audio, permette di aumentarne o attenuarne il valore durante la riproduzione senza alterare altri parametri. I metodi:

public int getMaxPitch()
public int getMinPitch()


ritornano, rispettivamente, la massima e la minima tonalità consentita, mentre:

public int setPitch(int millisemitones)
public int getPitch()

permettono di settare ed ottenere la tonalità corrente. I valori passati e ritornati da questi metodi sono espressi su base mille e rappresentano un aumento, se positivi, o una diminuzione, se negativi della tonalità corrente rispetto a quella originale.

 

RateControl
Permette il controllo della velocità di riproduzione di un contenuto e rappresenta il rapporto tra il media time e il Timebase di un player. Tale rapporto è espresso su base mille.
Utilizzando i metodi:

public int getMaxRate()
public int getMinRate()
public int getRate()

ritornano, rispettivamente, il rate massimo, minimo e attuale di un player, mentre

public int setRate(int millirate)

permette di settarne il valore.

 

RecordControl
Permette di controllare le operazioni di registrazione di un contenuto in riproduzione nel player.
La registrazione inizia con la chiamata al metodo:

public void startRecord()

e termina nel momento in cui viene invocato:

public void stopRecord()

Il metodo:

public void reset()

permette di "distruggere" il risultato della registrazione, mentre

public void commit()

completa la registrazione e procede al salvataggio effettivo dello stream registrato.

I dati registrati (il flusso proveniente dal player) sono inviati ad uno stream di output. Per far confluire i dati provenienti della registrazione in detto stream si utilizza il metodo:

public void setRecordStream(OutputStream stream)

Per limitare la quantità di dati, intesa come numero di byte, registrati si può utilizzare il metodo:

public int setRecordSizeLimit(int size)

Il metodo:

public void setRecordLocation(String locator)

infine, permette di stabilire dove i dati saranno salvati (come abbiamo visto, locator ha la forma di una URL).

 

StopTimeControl
Permette di controllare "il punto di arresto" di un player.

public long getStopTime()

ritorna il valore (in microsecondi) dell'istante in cui il player verrà bloccato. Tale valore è relativo al media time del contenuto in esecuzione sul player ed è quello precedentemente impostato con:

public void setStopTime(long stopTime)

 

TempoControl
E' il controllore del tempo (battute al minuto) di un player per la riproduzione di contenuti musicali (es. MIDI). I metodi:

public int getTempo()
public int setTempo(int millitempo)

consentono, rispettivamente, di ottenere e settare il tempo (espresso su base mille).

 

ToneControl
Permette il controllo della riproduzione di sequenze di toni definiti dall'utente, per mezzo del metodo:

public void setSequence(byte[] sequence)

sequence rappresenta la sequenza da riprodurre, espressa come un insieme di blocchi di coppie tono/durata. La forma generale è la seguente:

sequence = version tempo_definition resolution_definition block_definition sequence_event;

dove:

version: è il numero della versione;
tempo_definition: è la definizione degli attributi riguardanti il tempo;
resolution_definition: rappresenta la risoluzione desiderata;
block_definition: è la definizione dei blocchi (blocco di partenza e blocco di fine sequenza)
sequence_event: contiene le informazioni riguardanti i toni da riprodurre, contiene i seguenti sottocampi:

tone_event
block_event
volume_event
repeat_event

che servono a stabilire, rispettivamente il tono (nota e durata), il numero del blocco, il valore del volume e il numero di volte che il tono stesso deve essere ripetuto.

 

VideoControl
E' il controllo necessario alla realizzazione di player video. Il suo nome non deve trarre in inganno: come vedremo esso non si occupa della gestione del video inteso come contenuto multimediale (gestione del flusso video/audio in ingresso/uscita), ma solo del suo rendering sui dispositivi di output ( per agire sul contenuto, come abbiamo visto, esistono altri tipo di controlli come FramePositioningControl, MetadataControl, RateControl, RecordControl e VolumeControl che tratteremo tra breve). VideoControl estende GUIControl dalla quale eredita il metodo:

public Object initDisplayMode(int mode, Object arg)

che permette l'inizizializzazione del controllo video, stabilendo le modalità con cui il video deve riprodotto. Tali modalità possono essere:

USE_GUI_PRIMITIVE: di cui abbiamo detto a proposito del GUIControl;
USE_DIRECT_VIDEO: che a differenza del primo può essere utilizzato solo in ambiente MIDP (richiede il supporto lcdui). In questo caso il parametro arg da passare al metodo deve essere un oggetto Canvas o una sua estensione.
I metodi:

public void setDisplaySize(int width, int height)

public void setDisplayLocation(int x, int y)

public int getDisplayHeight()
public int getDisplayWidth()

public int getDisplayY()
public int getDisplayX()

permettono, rispettivamente, di stabilire ed ottenere le dimensioni e il posizionamento del video sullo schermo del dispositivo ( le coordinate di default sono 0,0 ).

public void setVisible(boolean visible)

permette di rendere o meno (passando true o false come parametro) visibile il video sul display, mentre con:

public void setDisplayFullScreen(boolean fullScreenMode)

permette di ottenere una visione a pieno schermo.

public int getSourceHeight()
public int getSourceWidth()

ritornano altezza e larghezza della sorgente video (solitamente corrispondono a quelle dello schermo).

Infine, il metodo:

public byte[] getSnapshot(String imageType)

permette di "fotografare il video" ottenendo un'istantanea del frame corrente.
Il parametro passato permette di stabilire il formato dell'immagine (l'unico attualmente supportato è quello PNG). I byte ritornati rappresentano il contenuto dell'immagine.

 

VolumeControl
Permette il controllo del volume delle sorgenti audio. Il livello del volume è controllabile per mezzo dei metodi:

public int getLevel()
public int setLevel(int level)

che ritornano e settano il valore corrente del volume, e

public void setMute(boolean mute)
public boolean isMuted()

che permettono di disabilitare la riproduzione audio e verificare se ci si trova in tale situazione.
Abbiamo visto come le MMA siano ricche di controlli tra loro distinti e indipendenti, questo permette la realizzazione di player dei relativi controlli molto "leggeri" e adattabili alle più diverse esigenze. Concludiamo riportando quì di seguito un esempio relativo ad un player video.


import java.io.*;
import javax.microedition.midlet.*;
import javax.microedition.io.*;
import javax.microedition.lcdui.*;


public class VideoPlayer extends MIDlet {

private static VideoPlayer instance;

private JavaVideoPlayer jvPlayer;
private int screenWidth = 100;
private int screenHeight = 100;
private String videoUrl;

/** Il costruttore */
public VideoPlayer() {
instance = this;
/** Recupera le informazioni relative alle dimensioni del display dal file JAD*/
String s = getAppProperty("screen-size");
videoUrl = getAppProperty("url");
if( s != null )
{
s = s.trim();
screenWidth = Integer.valueOf( s.substring(0, s.indexOf("x")) ).intValue();
screenHeight = Integer.valueOf( s.substring(s.indexOf("x")+1) ).intValue();
}
}

public void startApp() {
try
{
/** Crea un oggetto JavaVideoPlayer passando la url del video da riprodurre e le dimensioni*/
/** del display*/
jvPlayer = new JavaVideoPlayer(videoUrl, screenWidth, screenHeight);
} catch( Exception e)
{
e.printStackTrace();
}
/** Se la creazione del JavaVideoPlayer è riuscita viene visualizzato il player sul display*/
/** altrimenti viene lanciato un messaggio di errore */
if( jvPlayer != null)
Display.getDisplay(this).setCurrent(jvPlayer);
else
Display.getDisplay(this).setCurrent(new Alert("Error", "Unable to create Java Video Player", null, AlertType.ERROR));
}

public void pauseApp() {
}

public void destroyApp(boolean unconditional) {
instance.notifyDestroyed();
instance = null;
}
}

import java.io.*;
import javax.microedition.io.*;
import javax.microedition.lcdui.*;
import javax.microedition.media.*;
import javax.microedition.media.control.*;

public class JavaVideoPlayer extends Canvas {

/** Il player */
private Player player = null;
/** Il controllo video */
private VideoControl videoCtrl = null;
/** Il controllo audio */
private VolumeControl volumeCtrl = null;
/** Il controllo sul posizionamento dei frame */
private FramePositioningControl frameCtrl = null;
/** Il percorso per individuare il video da riprodurre */
private String resUrl = null;
/** Le dimensioni del display */
private int screenHeight;
private int screenWidth;
/** Modalità "pieno schermo" */
private boolean fullScreen = false;

/** Costruttore*/
public JavaVideoPlayer(String url, int w, int h) throws Exception
{
this.resUrl = url;
this.screenWidth = w;
this.screenHeight = h;
init();
}

/** Inizializza il player e crea i controlli associati */
public void init()
{
try{
/** "Legge" il vedio da uno stream di input. La risorsa in questo caso è sul dispositivo */
/** ma potrebbe essere su un server remoto ed essere letta con un socket o una HttpConnection */
InputStream is = getClass().getResourceAsStream(resUrl);
/** Ottiene un il player video dal manager */
player = Manager.createPlayer(is, "video/mpeg");
/** Crea il player */
player.realize();
/** Crea i controlli */
videoCtrl = (VideoControl) player.getControl("VideoControl");
volumeCtrl = (VolumeControl) player.getControl("VolumeControl");
frameCtrl = (FramePositioningControl) player.getControl("FramePositioningControl");
/* Inizializza il controllo video */
videoCtrl.initDisplayMode(VideoControl.USE_DIRECT_VIDEO, this);
int displayWidth = screenWidth*80/100;
int displayHeight = screenHeight*80/100;
/* Inposta le dimensioni e il posizionamento del video sul display */
videoCtrl.setDisplaySize(displayWidth, displayHeight);
videoCtrl.setDisplayLocation( (screenWidth-displayWidth)/2, (screenHeight - displayHeight)/2 );
videoCtrl.setVisible(true);
/** Porta il player nello stato PREFETCH */
player.prefetch();
} catch(Exception e){}
}

public void paint( Graphics g )
{
/** Si limita solo a scrivere la label Java Video Player i alto sul display */
g.fillRect(0,15,screenWidth, screenHeight-15);
int oldColor = g.getColor();
g.setColor(255, 255, 255);
g.fillRect(0,0,screenWidth,14);
g.setColor(255, 0, 0);
g.drawString("Java Video Player", screenWidth/2, 0, g.TOP | g.HCENTER);
g.setColor(oldColor);
}

/** Gestisce gli eventi derivanti dalla pressione dei tasti sul tastierino del dispositivo */
public void keyPressed(int keyCode)
{
try {
switch( keyCode )
{
case KEY_NUM0:
volumeCtrl.setMute( !volumeCtrl.isMuted() );
break;
case KEY_NUM1:
break;
case KEY_NUM2:
start_pause();
break;
case KEY_NUM3:
case KEY_NUM4:
break;
case KEY_NUM5:
stop();
break;
case KEY_NUM6:
break;
case KEY_NUM7:
rewind();
break;
case KEY_NUM8:
videoCtrl.setDisplayFullScreen(fullScreen = ! fullScreen);
break;
case KEY_NUM9:
forward();
break;
case KEY_POUND:
volumeCtrl.setLevel(volumeCtrl.getLevel()+10);
break;
case KEY_STAR:
volumeCtrl.setLevel(volumeCtrl.getLevel()-10);
break;
case UP:
case DOWN:
case LEFT:
case RIGHT:
case FIRE:
break;
}
}catch (Exception e){}
}
/** Fa partire il player o, se questo è già in esecuzione lo porta nello stato di pausa */
public void start_pause() throws Exception
{
if(player.getState() == Player.PREFETCHED)
{
videoCtrl.setVisible(true);
player.start();
}
else if (player.getState() == Player.STARTED)
player.stop();
}
/** Ferma il player e riposiziona riporta il "puntatore" all'inizio del video */
public void stop() throws Exception
{
player.stop();
player.setMediaTime(0);
}

/** Permette di spostare il "puntatore" nel frame successivo */
public void forward() throws Exception
{
frameCtrl.skip(1);
}

/** Permette di spostare il "puntatore" nel frame precedente */
public void rewind() throws Exception
{
frameCtrl.skip(-1);
}
}


Ecco come si presenta il player sull'emulatore MMA:




Figura 3
- Riproduzione di un video



Figura 4
- Modalità 'Full-Screen'


Riferimenti
http://www.iana.org/assignments/media-types/
http://www.nokia.com
http://www.ietf.org/rfc/rfc1889.txt
http://java.sun.com/j2me/

 
MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it