In questo primo articolo ci occuperemo in particolare di GWT, della libreria Elemental e di come si possa utilizzare una parte di WebRTC, precisamente getUserMedia, in un progetto GWT guadagnando, dall’interno di una applicazione web, l’accesso alla webcam e conservando la possibilità di scrivere codice Java e riutilizzare, nel browser, anche librerie sviluppate per altri contesti.
Introduzione
GWT (Google Web Toolkit) [1], è il toolkit per lo sviluppo in Java di applicazioni “browser based” nato come “prodotto” Google ma oramai trasformatosi a tutti gli effetti un progetto open-source, è diventato in pochi anni uno dei framework di riferimento per lo sviluppo di applicazioni web complesse e il suo utilizzo sembra essere in grande crescita, come riportato da Web Frameworks Usage Statistics [2], che ne rileva una crescita d’impiego negli ultimi 12 mesi intorno al 90%.
GWT mette a disposizione dello sviluppatore Java molte possibilità: la compilazione del codice Java in JavaScript; un sistema di “compilazione condizionale” per gestire le incompatibilità tra i browser; un set di widget ben progettato; dei sistemi di comunicazione con i servizi server-side “amichevoli”, almeno per gli sviluppatori Java, e molto altro.
A fronte di tutta questa potenza, però, l’esigenza di rendere il codice il più possibile indipendente dal tipo di browser rende alle volte piuttosto laborioso l’utilizzo delle più recenti feature dei browser, soprattutto quando queste feature sono supportate solo da alcuni browser e non ancora standardizzate.
L’argomento
In questo articolo, primo di una breve serie, ci poniamo come obiettivo di mostrare come si possa utilizzare una parte di WebRTC [3], precisamente getUserMedia in un progetto GWT guadagnando, dall’interno di una applicazione web, l’accesso alla webcam e conservando la possibilità di scrivere codice Java e riutilizzare, nel browser, anche librerie sviluppate per altri contesti.
Se in questa prima parte ci occuperemo sostanzialmente solo di capire come la libreria GWT “to the metal” chiamata Elemental ci possa aiutare ad utilizzare WebRTC, nelle prossime puntate vedremo come utilizzare WebGL [4] e, con un pizzico di funambolismo informatico, come compilare in JavaScript la libreria NyARToolkit [5], per realizzare una applicazione “browser based” per fare realtà aumentata (propriamente marker based real-time augmented reality) interamente lato client.
In ogni puntata di questa serie, oltre a cercare di descrivere gli aspetti rilevanti delle tecnologie e degli strumenti utilizzati, pubblicheremo anche il codice d’esempio pronto per l’uso, che potrete scaricare come allegato dal menu a destra.
Gli strumenti
Per i lettori più impazienti, diciamo subito che è possibile vedere una demo all’indirizzo [6], che richiede il browser Google Chrome per funzionare; il codice, oltre che nell’allegato, si trova all’indirizzo [7] e la versione compilata è reperibile all’indirizzo riportato in [8] in formato WAR per semplicità ma, non utilizzando nessun servizio lato server, può essere pubblicata su qualsiasi web server.
GWT Elemental Library
A partire dalla versione 2.5, GWT include Elemental [9], una libreria forse non molto conosciuta, e di sicuro poco documentata, ma dalle caratteristiche davvero interessanti per chi voglia utilizzare con GWT le feature HTML5 più recenti dei browser (almeno di quelli basati su WebKit).
Come si legge nella presentazione ufficiale, allo scopo di creare una libreria che potesse includere “every HTML5 feature” disponibile sui browser derivati da WebKit, Elemental viene generata automaticamente a partire dai file WebIDL [10] utilizzati dai motori JavaScript e quindi risulta relativamente aggiornata rispetto alle evoluzioni, spesso frenetiche, dei browser.
A fronte di una scarsissima documentazione, usare Elemental in un progetto GWT non è affatto complesso: basta scaricare l’ultima versione di GWT da [11] (la 2.5.1 nel nostro caso), creare un progetto, ad esempio con Eclipse come di consueto, importare nella build path gwt-elemental.jar e, infine, aggiungre nel file .gwt.xml del modulo la riga:
Siamo pronti per utilizzare Elemental.
WebRTC getUserMedia
Nonostante negli ultimi anni ci siano stati molteplici tentativi di definire uno standard per permettere l’accesso alla webcam e al microfono di un computer dall’interno di una applicazione web (quindi eseguita nella “gabbia” del browser), solo di recente, con l’evoluzione di WebRTC, questa esigenza è stata effettivamente soddisfatta. Una descrizione dei tentativi di standard che si sono succeduti è riportata in [12].
WebRTC è una tecnologia che ha come obiettivo quello di rendere possibili le comunicazioni real time tra browser e, nonostante le innumerevoli complessità incontrate, avendo tra i suoi supporter nomi quali Google, Mozilla e Opera, la specifica sta rapidamente trovando implementazione sui alcuni dei browser maggiormente diffusi. Di fatto, come riportato nell’articolo in [13], all’inizio di febbraio è stato fatto un esperimento pubblico di interoperabilità tra Chrome e Mozilla allo scopo di mostrare come lo standard stia giungendo a maturità.
Benchè WebRTC abbia degli scopi di respiro ben più ampio rispetto a quelli che ci proponiamo in questo articolo, la prima componente di WebRTC che ha trovato implementazione relativamente stabile su Chrome, e attualmente disponibile su tutte le piattaforme, è quella che permette appunto di accedere allo stream video proveniente da webcam: getUserMedia.
L’accesso allo stream video attraverso getUserMedia e il suo effettivo utilizzo in una applicazione web richiede l’interazione di diverse “componenti” HTML5 (video, objectURL, canvas) e, come vedremo subito di seguito non è priva di qualche tecnicismo che vale la pena di osservare con attenzione.
GWT Elemental + WebRTC getUserMedia + tag video e canvas…
…ossia come realizzare una applicazione web per fare istantanee con la webcam. Vediamo come entrano in gioco le diverse componenti e quali passi occorre seguire per integrarli al meglio
che servirà da contenitore per lo stream proveniente dalla webcam. Creare un video element è estremamente semplice con Elemental:
VideoElement videoElement = Browser.getDocument().createVideoElement();
ed è altrettanto semplice specificare una sorgente:
videoElement.setSrc(url);
o, manipolando il DOM come da specifica [14], aggiungerne molteplici in modo che il browser possa scegliere quella supportata:
SourceElement src = Browser.getDocument().createSourceElement(); src.setType(type); src.setSrc(source); videoElement.appendChild(src);
Purtroppo, a fronte della indubbia comodità di utilizzare Elemental per accedere alle features “a basso livello” del browser, utilizzare gli Element di Elemental (che derivano da elemental.dom.Element) assieme ai plain-old-GWT-widget (che invece utilizzano DOM elements che derivano da com.google.gwt.dom.client.Element) dobbiamo fare qualche altro passaggio.
Innanzi tutto un cast “nativo” (metodo definito in ElementalUtils nei sorgenti):
public final native static com.google.gwt.dom.client.Element castElementToElement(elemental.dom.Element e) /*-{ return e; }-*/;
e quindi, usando la classe Widget, possiamo creare un wrapper intorno al VideoElement:
public class ElementalVideoWidget extends Widget { private VideoElement videoElement; public ElementalVideoWidget() { videoElement = Browser.getDocument().createVideoElement() ; setElement(ElementalUtils.castElementToElement(videoElement)); } public void addSource(String source, String type) { SourceElement src = Browser.getDocument().createSourceElement(); src.setType(type); src.setSrc(source); videoElement.appendChild(src); } public VideoElement getVideoElement() { return videoElement; } }
dove, peraltro, possono trovare spazio alcuni metodi per rendere meno verboso l’utilizzo del VideoElement (p.e. addSource).
Ottenuto un Widget possiamo direttamente usare il video:
ElementalVideoWidget video = new ElementalVideoWidget(); video.addSource("http://www.w3schools.com/html/mov_bbb.mp4", "video/mp4"); video.addSource("http://www.w3schools.com/html/mov_bbb.ogg", "video/ogg"); RootPanel.get().add( video );
Come detto, avere a disposizione il tag video è essenziale per il nostro sviluppo in quanto
getUserMedia & createObjectUrl
La specifica di WebRTC prevede che, per accedere a quelli che sono chiamati “user media stream” (video da webcam e audio da microfono, fuori dal nostro scopo) l’applicazione debba utilizzare la funzione
navigator.getUserMedia(options, successCallback, failCallback)
rinominata, su Chrome, navigator.webkitGetUserMedia, con un chiaro riferimento a WebKit in quanto ancora non standard. Questa funzione provvede a chiedere all’utente l’autorizzazione ad accedere agli stream richiesti, si occupa del setup e li passa alla callback [16].
Natualmente Elemental supporta getUserMedia, ma in questo caso HTML5 risulta essere un bersaglio troppo mobile anche per Elemental e sulla versione corrente di Chrome la getUserMedia disponibile genera codice non utilizzabile (come risultano inutilizzabili buona parte delle demo che si trovano in rete, probabilmente proprio per questo cambiamento).
Per usare getUserMedia siamo quindi costretti ad abbandonare anche Elemental e scrivere direttamente codice “nativo”, JavaScript insomma,. Grazie a JavaScript Native Interface (JSNI) [17], questo è agevole semplicemente definendo (sempre nella classe ElementalUtils nei sorgenti) un metodo statico:
public native static boolean getUserVideo(UserMediaCallback callback) /*-{ if(navigator.webkitGetUserMedia) { navigator.webkitGetUserMedia( {video: true, toString: function() {return "video";}}, function(stream) { var s = window.URL.createObjectURL(stream); $entry(callback.@...::onSuccess(Ljava/lang/String;)(s)); }, function() { $entry(callback.@...::onFail()()); }); return true; } else { return false; } }-*/;
dove si vede anche come sia possibile passare un oggetto Java (UserMediaCallback) a un metodo nativo Javascript.
Va osservato che per poter essere utilizzato come source del tag video, lo stream che viene passato alla callback di webKitGetUserMedia deve essere trasformato in un URL. A renderlo possibile ci pensa ancora un altra specifica HTML5 (nata questa volta per permettere l’accesso a file dal browser) [18] e viene utilizzata nella linea di codice
var s = window.URL.createObjectURL(stream);
L’URLl così generato potrà essere direttamente utilizzato dal video.
Possiamo quindi finalmente scrivere l’intero EntryPoint:
public class ElementalGetUserMediaMinimal implements EntryPoint { @Override public void onModuleLoad() { boolean res = ElementalUtils.getUserVideo(new UserMediaCallback() { @Override public void onSuccess(String url) { ElementalVideoWidget video = new ElementalVideoWidget(); video.getVideoElement().setSrc(url); video.getVideoElement().play(); RootPanel.get().add(video); } @Override public void onFail() { Window.alert("FAIL, unauthorized ?"); } }); if(!res) Window.alert("FAIL, no webkitGetUserMedia Support ?"); } }
dove è necessario ricordare di chiamare il metodo play() dell’oggetto video, dopo aver inserito l’URL della sorgente, altrimenti il video non partirà.
Sebbene necessario per visuallizzare lo stream proveniente dalla webcam, l’oggetto video (VideoElement) non fornisce accesso al suo contenuto e quindi non permette alla nostra applicazione di elaborare lo stream. Per poter avere accesso allo stream video l’unica possibilità è quella di copiarlo su un canvas (il tag HTML5 ) e poi utilizzare i metodi dell’oggetto canvas per accedere ai fotogrammi.
Creare un canvas, sempre con Elemental, è ancora una volta semplice,
CanvasElement canvas = Browser.getDocument().createCanvasElement();
e per effettuare la copia del video sul canvas bastano le seguenti righe di codice:
canvas.setWidth( video.getVideoElement().getVideoWidth()); canvas.setHeight( video.getVideoElement().getVideoHeight()); elemental.html.CanvasRenderingContext2D ctx = (CanvasRenderingContext2D) canvasElement.getContext("2d"); ctx.drawImage(videoElement, 0, 0);
dove le prime due righe servono a garantire che il canvas abbia le medesime dimensioni del video (la cui risoluzione non è nota in anticipo essendo dipendente dall’hardware, dal browser, dal sistema operativo).
A tal proposito va osservato che la risoluzione del video stream non è immediatamente disponibile al browser al momento della chiamata alla callback e quindi i metodi video.getVideoElement().getVideoWidth() e getVideoHeight() potrebbero non restituire i valori corretti. Per essere sicuri di non utilizzare il video prima della inizializzazione la specifica mette a disposizione l’evento loadedmetadata che l’applicazione può utilizzare per essere informata della effettiva disponibilità delle informazioni sul video:
video.getVideoElement().addEventListener("loadedmetadata", new EventListener() { @Override public void handleEvent(Event evt) { … } });
Una volta copiato il video sul canvas è possibile estrarne il contenuto utilizzando il metodo canvas.toDataUrl(type) che restituisce il contenuto del canvas sotto forma di data URL (data URI secondo l’originale RFC [19]) direttamente utilizzabile, ad esempio, per popolare immagini, e.g.
Image img = new Image(canvas.toDataUrl("image/png"));
Come immaginabile, e visibile anche dalla demo (per Chrome) [6], una volta trasformati i fotogrammi del video in immagini la loro manipolazione è estremamente semplice.
Conclusioni
Come risulta chiaro dal lavoro svolto fino a questo punto, nel seguire questa strada per raggiungere l’obiettivo che ci siamo posti dobbiamo riconciliare almeno tre mondi: Elemental, i widget GWT che, anche se spesso considerati poco “appealing”, sono a nostro avviso uno strumento eccezionale con cui lavorare, e le vorticose evoluzioni di Chrome.
Vista l’indubbia complicatezza di rendere utilizzabili da GWT alcune feature di Chrome e di aggirare anche qualche “problema” di Elemental scrivendo pezzi di codice nativo potrebbero venire naturali due domande: la prima è “Non sarebbe meglio scrivere direttamente in JavaScript?”; la seconda è “Non sarebbe meglio abbandonare Elemental e scrivere tutto usando solo JSNI?”.
Entrambe le domande, pur rispettabilissime, a nostro parere hanno risposta negativa. Se infatti procedessimo usando solo JavaScript perderemmo, come detto anche nei paragrafi precedenti, l’opportunità di utilizzare il compilatore di GWT per “portare” sul browser le librerie che nei prossimi capitoli ci serviranno; oltre a questo perderemmo del tutto anche la comodità (magari opinabile ma da noi ritenuta essenziale) di scrivere codice in Java (tipato, compilato, object-oriended nel modo classico) invece che in JavaScript.
Abbandonare invece Elemental è alla fin fine una possibilità che nella stesura del codice di questo articolo abbiamo accarezzato anche noi: la parte di WebRTC che utilizziamo è piccola (sostanzialmente un solo metodo) e malfunzionante al punto che l’implementazione JSNI si è resa comunque necessaria, video ha una specifica (per quel tanto che lo abbiamo usato noi) relativamente breve e, se dovessimo basarci su quello di cui abbiamo avuto bisogno, anche canvas (e l’interazione video/canvas) sarebbe stato semplice da wrappare con codice nativo.
D’altro canto quello che Elemental mette a disposizione è l’intera specifica degli oggetti con tutti i metodi che erano presenti al momento della generazione della libreria (provate a guardare la quantità di metodi che CanvasElement e CanvasRenderingContext2D mettono a disposizione) risparmiandoci quindi di dover creare a mano metodi nativi ad ogni nuova caratteristiche che vogliamo esplorare.
Sostanzialmente, a nostro parere, il costo in termini di complicatezza che dobbiamo pagare per aggirare qualche problema di Elemental e qualche idiosincrasia tra GWT “classico” e GWT “to the metal” è ampiamente ripagato dal fatto di avere preconfezionati i wrapper per la gran parte degli oggetti che ci possono servire.
Riferimenti
[1] Google Web Toolkit
https://developers.google.com/web-toolkit/
[2] Statistiche sull’adozione dei framework
http://blog.websitesframeworks.com/2013/03/web-frameworks-statistics-174/
[3] WebRTC
[4] WebGL
[5] NyARToolkit
http://nyatla.jp/nyartoolkit/wp/?page_id=198
[6] La demo del progetto
http://www.jooink.com/experiments/ElementalGetUserMediaDemo/
[7] Il codice
http://code.google.com/p/elemental-getusermedia-demo/source/…
[8] La versione compilata
http://elemental-getusermedia-demo.googlecode.com/files/ElementalGetUserMediaDemo.war
[9] Elemental
https://developers.google.com/web-toolkit/articles/elemental?hl=en
[10] WebIDL
[11] GWT
https://code.google.com/p/google-web-toolkit
[12] Capturing Audio & Video in HTML5
http://www.html5rocks.com/en/tutorials/getusermedia/intro
[13] Alfonso Maruccia, “Mozilla e Google si videochiamano con WebRTC”
http://punto-informatico.it/3709886/PI/News/mozilla-google-si-videochiamano-webrtc.aspx
[14] video-element WHATWG
http://www.whatwg.org/specs/web-apps/current-work/ – the-video-element
[15] Local Media Stream
http://dev.w3.org/2011/webrtc/editor/getusermedia.html#idl-def-LocalMediaStream
[16] Get User Media
[17] JSNI
https://developers.google.com/web-toolkit/doc/latest/DevGuideCodingBasicsJSNI
[18] The createObjectURL static method
http://www.w3.org/TR/FileAPI/ – dfn-createObjectURL
[19] RFC 2397 “The “data” URL scheme”
http://tools.ietf.org/html/rfc2397