Youtube et JavaFX via MediaView

Bijour …

Je suis encore et toujours en train de m’amuser avec JavaFX, et comme je suis un gros noob et que j’ai rien réussi à faire avec Jiwa (et que deezer ne me file toujours pas de clé pour leur API), j’ai décidé de me tourner vers Youtube qui fournit une API ( les clés pour leur API ne semblent pas à servir pour grand chose, l’essentiel de l’API (un fichier XML/ATOM) étant accessible sans être identifié …).

L’appli finale ressemble étrangement à celle crée pour Jiwa, à la seul différence qu’elle embarque un lecteur vidéo (depuis l’API 1.2.1 de javaFX il est possible de jouer des flux rtsp … le support est donc encore un peu chaotique (il manque des codec) : la vidéo joue sans le son ou l’inverse, le chargement du média s’effectue mais il n’arrive pas à le lire …, certaine fonction lié au média ne fonctionne pas (onStalled et onBuffering)).

La tête de l’application finale (super moche … encore un concept) :

youtubeFX

Néanmoins malgrès tout les problèmes du support vidéo streaming, l’effet escompté est bien là … la lecture de la vidéo bouffe beaucoup moins de ressource (faut dire que la vidéo est au format mpeg4-sp (176*140) pour les téléphones portable donc … ;)).

La recherche des vidéos est effectuée en XML, j’aurais bien voulu utiliser l’objet AtomTask pour récupérer les données contenues dans le XML renvoyé par Youtube, mais malheureusement, dans leur API (celle de javaFX, pas de YouTube ^^), ils n’ont pas prévue qu’il puisse y avoir plusieurs médias dans le « content », donc j’ai du le parser à la main et faire un beau hack pour choper le flux.

Sur le plan technique, au lieu d’utiliser le MediaView, j’aurai pu utiliser la technique de Rakesh Menon en affichant une page web avec le player « chromeless de youtube » via l’API JS, mais ça aurait enlevé l’intêret de l’application puisque le but est de se débarasser du lecteur Flash.

Le temps de développement est super court (j’ai du mettre plus de temps à écrire l’article sur le blog qu’à developper l’appli ;)), par contre, y’a déjà pas mal de hack pour rendre l’appli plus rapide/réactive, et il n’y a malheureusement aucune fonction qui permettent de déterminer si on charge la vidéo ou non (le onBuffering ne fonctionne pas :s).

Donc c’est assez naze d’avoir une appli de streaming sans état d’avancement dans le chargement du média mais comme y’a pas grand chose en français sur JavaFX et les média … je post le code (en espérant que le support de Java soit meilleur plus tard ^^) :

/*
 * Main.fx
 *
 * Created on 9 oct. 2009, 20:18:51
 */

package youtubefx;

import javafx.stage.Stage;

import javafx.scene.Scene;
import javafx.scene.text.Text;
import javafx.scene.media.MediaPlayer;
import javafx.data.pull.PullParser;
import javafx.io.http.HttpRequest;
import javafx.data.pull.Event;
import javafx.scene.control.Button;
import javafx.scene.control.TextBox;
import javafx.scene.layout.VBox;
import javafx.scene.layout.HBox;
import javafx.scene.control.ListView;
import javafx.ext.swing.SwingListItem;
import javafx.scene.input.MouseEvent;
import javafx.scene.media.MediaView;
import javafx.scene.media.Media;
import javafx.scene.control.ProgressBar;
import javafx.scene.Group;
import javafx.scene.shape.Rectangle;
import javafx.scene.paint.Color;
import javafx.scene.CustomNode;
import javafx.scene.Node;

/**
 * @author Gaetan Grigis
 */
class Splash extends CustomNode {
    var rect = Rectangle {x:0,y:0,width: 400,height: bind scene.height,fill: Color.BLACK};
    var txt = Text{content: "Hello this is just a simple test to use MediaView\n"
                          "and MediaPlayer with Youtube\n \n"
                          "As the support of streaming by JavaFX is new\n"
                          "sometimes it crash or doesn't work as expected\n"
                          "because of unsupported codec (video or music\n"
                          "isn't played in this case ...)\n \n"
                          "(Click somewhere here to close this ...\n"
                          "and try this ...)",x: 50,y: 50,fill: Color.AQUAMARINE};
    var groups = Group {
        content: [rect,txt],
        onMouseClicked: function( e: MouseEvent ):Void {rect.width=0;txt.content="";txt.x=0;txt.y=0}
    }

    override function create():Node {return groups;}
}
var item : SwingListItem[] = [];
var xml : PullParser = PullParser {documentType: PullParser.XML};
var mus = [];
//UI items
var scene = Scene {width: 400,height: 250};
var searchTxt = TextBox{width:bind (scene.width - 276),text: "Type your text .."};
var searchBtn = Button{text: "Search",width: 100,action: function(){searchItem(searchTxt.text)};};
var listView : ListView = ListView{width:bind (scene.width-176),height: bind (scene.height - searchTxt.height),items: bind item};
var SearchBox = HBox {content: [searchTxt,searchBtn],width: bind scene.width};
var PlayBox = VBox{content:[SearchBox,listView],width: bind (scene.width - 176),height: bind scene.height};
var player = MediaPlayer{autoPlay:true};
var info = Text {content: bind "{player.currentTime.toSeconds()}/{player.media.duration.toSeconds()} Sec.\nStatus : {player.status}\nClick on the progressBar\nif it doesn't start\n\tStatus 2 = playing\n\tStatus 0 = Loading/Crashed", };
var pg = ProgressBar{progress: bind ProgressBar.computeProgress(player.media.duration.toSeconds(),player.currentTime.toSeconds()),onMouseClicked:function(e:MouseEvent){if(player.status!=2){player.play();}else{player.pause();}}};
scene.content = HBox{content:[Splash{},PlayBox,VBox{content: [MediaView{visible:true, cache: true, mediaPlayer: player},info,pg]}]};

function searchItem(search: String)
{
    item = [];//on reset à la barbare 😉
    var connexion : HttpRequest = HttpRequest {
        method: HttpRequest.GET
        location: "http://gdata.youtube.com/feeds/api/videos?v=2&lr=en&format=6&orderby=published&safeSearch=strict&q={search}&max-results=50"
        onInput: function(is: java.io.InputStream) {
            try {
                xml.input = is;
                var it = SwingListItem{};
                var i  = 0;
                xml.onEvent = function(event: Event) : Void{
                    if( event.qname.prefix == "media" )
                    {
                        if(event.qname.name == "content" and event.typeName == "START_ELEMENT" and event.getAttributeValue("type") == "video/3gpp" )
                        {
                            //can't get the yt:format, so as we know that mpeg is second we just increment ...
                            it.value = event.getAttributeValue("url");
                            i++;
                        }
                        if(event.qname.name == "title" and event.text!="" and event.typeName == "TEXT"){it.text = event.text;}
                        if(it.text !="" and it.value!="" and i > 1 ){
                            insert it into item;it = SwingListItem{};
                        }
                    }
                };
                xml.parse();
            } finally {is.close();}
        }
    };connexion.start();
}

listView.onMouseClicked = function (e:MouseEvent):Void{
    player.stop();
    listView.disable=true;
    //hack to stop playing while another is loading
        player.media.source = null;
        player.media = null;
    player.media = Media{source:item[listView.selectedIndex].value.toString(),};
    player.play();
    listView.disable=false;
}
Stage {title: "ytFX",scene: scene,onClose:function(){player.pause();player.media.source = null;player.media = null;}}

Comme dit, le code est moche, y’a du hack partout plein de syntaxe différente pour des trucs similaires …