Harmonyfx – JavaFx Media Player

Introduction

Before you start read the rest of the article, watch the demo below

and the longer one (with drag & drop + search demo: 4:19 minutes)

This article has been in the making a while, and has sort of become a labour of love. When I first started the sole purpose of this article was to gain a deeper understanding of some of JavaFX features APIs, (CustomNode, Storage & Resource, RESTful Web Service using HttpRequest & pullparser, Binding with java object, drag and drop, effects, animation, “3D” etc) I needed an arena in which to perform this self study, so what I decided to do was construct a workable/searchable MP3 player. The MP3 player would work of MP3 ID3 tag metadata which would be obtained using myID3 a java ID3 tag library.

In essence this is what this article is all about.

Overview
Essentially HarmonyFx (the codename ;)) looks like this. It is made up of a number of different consituent CustomNode (Custom Control),

harmonyfx
harmonyfx2

How It Is Intended to Work
I have tried to write this application in a logical manner, and I hope it works as most people would expect it to work. So without further ado, let me delve in to how I intended Harmonyfx to work.

When Harmonyfx is run it will examine the Storage (Library) and tray read all files, that previously saved, Read the metadata (ID3) tag, and arrange it in shelf (cover flow), and download the Album art based on the Album name from last.fm web service. User can add songs in the library by drag and drop folder or files into harmonyfx.

If Harmonyfx is shutdowned, the library updated again with last changed library in application.


Library – Resource & Storage
Here this the class for save and load library from storage:

public class Library {
    var storage:Storage;
    var resource:Resource;

    postinit{
        var storeExist = checkStore();
        if (storeExist){
            // load songs
            PlaylistView.filelist = loadSongs();
        }

        FX.addShutdownAction(function():Void{saveIntoLibray (PlaylistView.filelist)});
    }
    
    public function loadSongs ():String[]{
        var songs:String[];

        var inp: InputStream = resource.openInputStream();

        try {
            var reader = new BufferedReader(
                        new InputStreamReader(inp));
            var song:String;
            try{
                while ((song = reader.readLine()) != null){
                    insert song into songs;
                }

            }catch(ex:IOException){
                println("error load library: {ex.getMessage()}");
            }

        } finally {
            inp.close();
        }
        return songs;
    }

    public function saveIntoLibray (songs:String[]) {
        var store_exists = checkStore();
        if (store_exists == true)
            loadSongs();

        var outp: java.io.OutputStream = resource.openOutputStream(true); // override old
        try{
        //Store data including newline character
        var newline:String = "\n";
            for (song in songs){
                outp.write(song.getBytes());
                outp.write(newline.getBytes());
            }
        }catch(e:IOException){
            println("save lib exception : {e.getMessage()}");
        }finally{
            outp.close();
        }
    }

    public function checkStore() : Boolean {

        var ret: Boolean = false;
        try {
            storage = Storage {
                source: "harmonyfxlibrary"
            };

            resource = storage.resource;
            ret = false;

        } catch (e : IOException ) {
            resource = storage.resource;
            ret = true;
        }
        ret = true;
    }
}

Drag & Drop & Binding Java Object from JavaFX
Harmonyfx actually allows more music to be added into it’s library. This is done via drag and drop, where the user is able to drag a entire directory or just single file(s). This is done by dragging items to “client area” of Harmonyfx.
since javafx 1.2 hasn’t include API to handle this task, i decide to wrap swing JPanel and use java API to handle this drag & drop task.

class Background extends SwingComponent{
    public var background:  JPanel;
    public override function createJComponent(){
        background = new JPanel();
        background.setBackground(java.awt.Color.BLACK);
        background.setPreferredSize(new Dimension(width, height));
        background.setDropTarget(new DropTarget(background,DnDConstants.ACTION_COPY_OR_MOVE,
            dndlistener, true));

        background.setRequestFocusEnabled(true);
        return background;
    }
}

and the drag & drop listener implementation is like this :

package harmonyfx.dndListener;
// file: DnDListener.java
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.io.File;
import java.util.Observable;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.List;
import java.util.Vector;

public class DnDListener extends Observable implements DropTargetListener {

    protected boolean acceptableType;
    public String[] files;

    public DnDListener() {
    }

    @Override
    public void dragEnter(DropTargetDragEvent dtde) {
        checkTransferType(dtde);
        acceptOrRejectDrag(dtde);
    }

    @Override
    public void dragExit(DropTargetEvent dte) {
    }

    @Override
    public void dragOver(DropTargetDragEvent dtde) {
    }

    @Override
    public void dropActionChanged(DropTargetDragEvent dtde) {
        acceptOrRejectDrag(dtde);
    }

    @Override
    public void drop(DropTargetDropEvent dtde) {
        if ((dtde.getDropAction() & DnDConstants.ACTION_COPY_OR_MOVE) != 0) {
          dtde.acceptDrop(dtde.getDropAction());
          Transferable transferable = dtde.getTransferable();

          try {
                boolean result = dropFile(transferable);
                dtde.dropComplete(result);
          } catch (Exception e) {
                dtde.dropComplete(false);
                System.out.print("exception: "+e.getMessage());
          }
        }else {
          dtde.rejectDrop();
        }
    }

    protected boolean acceptOrRejectDrag(DropTargetDragEvent dtde){
        int dropAction = dtde.getDropAction();
        int sourceActions = dtde.getSourceActions();
        boolean acceptedDrag = false;

        if (!acceptableType || (sourceActions & DnDConstants.ACTION_COPY_OR_MOVE) == 0) {
            dtde.rejectDrag();
        } else if ((dropAction & DnDConstants.ACTION_COPY_OR_MOVE) == 0) {
            dtde.acceptDrag(DnDConstants.ACTION_COPY);
            acceptedDrag = true;
        } else {
            dtde.acceptDrag(dropAction);
            acceptedDrag = true;
        }

        return acceptedDrag;
    }

    protected void checkTransferType(DropTargetDragEvent dtde) {
        acceptableType = dtde.isDataFlavorSupported(DataFlavor.javaFileListFlavor);

    }

    protected boolean dropFile(Transferable transferable) throws IOException,
        UnsupportedFlavorException, MalformedURLException {

        List fileList = (List) transferable.getTransferData(DataFlavor.javaFileListFlavor);
        int i = 0;
        List<String> list = new Vector<String>();
        String[] fls = new String[fileList.size()];
        for (Object f : fileList){
            File file = (File) f;
            exploreDir(file, list);
        }

        if (list != null)
            setFiles(list);
        return true;
    }

    void exploreDir(File file, List<String> list){
        if (file.isFile()){
            if (file.getName().toLowerCase().endsWith(".mp3")){
                try{
                    list.add(file.toURL().toString());
                }catch(Exception ex){
                    System.out.println("Error conversion: "+ex.getMessage());
                }
            }
        }else if (file.isDirectory()){
            File fs[] = file.listFiles();
            for (File f : fs)
                exploreDir(f, list);
        }

    }

    public void setFiles(List<String> f){
        files = new String[f.size()];
        int i=0;
        for (String s: f)
            files[i++] = s;

        startNotification();
    }

    private void startNotification(){
        setChanged();
        notifyObservers();
    }

    public String[] getFiles(){
        return files;
    }
}

and because i must bind the files that dropped, i implements (inherit) this class with Observable class, so whenever files/folder dragged, this class will notify the Adapter:

// file: DnDListenerAdapter.fx
import java.util.Observer;
import java.util.Observable;

public class DnDListenerAdapter extends Observer{
    public var files:String[];
    public-init var dndListener:DnDListener on replace{
        dndListener.addObserver(this);
    }
    public override function update(observable:Observable, arg:Object){
        FX.deferAction(
            function():Void{
                files = dndListener.getFiles();
            });
    }
}

there are a couple example that help me to implement this one, one from Eric and other from Michael Heinrichs (about binding java object in JavaFx)

Custom ListView
Current Default ListView control can’t display itemview except in string format, so I make custom listview to get better UX when displaying list of song for an album. My ListView Implementation is developed based on Tweeter application example at javafx/samples

Flip & Cover Flow animation with PrespectiveTransform
nothing special with this, i just rewrite the samples codes from javafx samples ;) check this 2 links samples out
1. Displayshelf
2. PageFlip

MP3s / ID3
Harmonyfx relies heavily on ID3 tag metadata that may or may not be available within scanned files. so I had a hunt about and found an excellent free ID3 library for java which is called MyID3 which reads both ID3v1 and ID3v2 tags. It is very easy to use and is obviously included in HarmonyFx.

Here is what it looks like to read a ID3 tag for a given file, where i read mp3 ID3 tag whenever mp3 file locataion is updated using javafx trigger (on replace).

public class Song{
    public var album:String;
    public var title:String;
    public var artist:String;
    public var albumArt:Image;

    public var url:String on replace{
        var id3 = ID3Reader{};
        id3.fileName = url.replaceAll("file:/", "");

        def albm = id3.getAlbum();
        album = if (albm != null ) albm else "";

        def ttle = id3.getSongTitle();
        title = if (ttle != "") ttle else{
            url.substring(url.lastIndexOf("/")+1).replaceAll(".mp3","");
        }
        artist = id3.getArtist();
    };
   // another code goes here

Web Service
I Use Last.fm web service to obtain album art for specific album. here this how I use HttpRequest and Pullparser to obtain the album art:

 public function getImage (artist:String,album:String) {
        def artst = artist.replaceAll(" ", "%20");
        def albm = album.replaceAll(" ", "%20");
        def url= "http://ws.audioscrobbler.com/2.0/?method=album.search&amp;album={albm}&amp;api_key=1d819e1201e75b96724a818c85e7f730";

        var p:PullParser;
        var h:HttpRequest;
        h = HttpRequest{
                location: url
                onException: function(exception: Exception) {
                    print("exception: {exception.getMessage()}");
                }

                onInput: function(input) {
                    if (album != ""){
                        try{
                            println("album");
                            var attval : String;
                            p = PullParser {
                                documentType: PullParser.XML
                                input: input
                                onEvent: function(event) {
                                    if (event.type == PullParser.START_ELEMENT){
                                        if (event.qname.name == "image"){
                                              var qAttr : QName = QName {name : "size"};
                                              attval = event.getAttributeValue(qAttr);
                                        }
                                    }else
                                    if (event.type == PullParser.END_ELEMENT){
                                        if (event.qname.name == "image"){
                                              if (attval == "small"){
                                                  //println("small: {event.text}");
                                              }else if (attval == "medium"){
                                                  //println("medium: {event.text}");
                                              }else if (attval == "large"){
                                                  println("large: {event.text}");
                                                  if (event.text != "" and artImage.url == "")
                                                  artImage = Image{
                                                        url: event.text;
                                                        width: 200
                                                        height: 200
                                                        preserveRatio: true
    //                                                  placeholder: Image {
    //                                                        url: "{__DIR__}AlbumArt.png"
    //                                                  }
                                                  }
                                              }else if (attval == "extralarge" and artImage.url == ""){
                                                  println("extralarge: {event.text}");
                                                  if (event.text != "")
                                                  artImage = Image{
                                                      url: event.text;
    //                                                  placeholder: Image {
    //                                                        url: "{__DIR__}AlbumArt.png"
    //                                                  }
                                                  }
                                              }
                                        }
                                    }
                                }
                            };

                            p.parse();
                            p.input.close();
                        }catch(exc:Exception){
                            println("msg: {exc.getMessage()}");
                        }finally{
                            p.input.close();
                        }
                    }
                }
                
                onDone: function() {
                    println("DONE!\nartImageurl: {artImage.url}");
                }
        };

        h.start();
    }
}


What Do You Think
Anyway that’s it, want to try it by yourself, Download Harmonyfx netbeans project from here: Harmonyfx.zip.

btw, comments are welcome :)

References
1. http://javafx.com/samples/
2. http://blogs.sun.com/michaelheinrichs/entry/binding_java_objects_in_javafx
3. http://www.fightingquaker.com/myid3/
4. http://java.sun.com/javafx/1.2/docs/api/
5. javafx drag and drop

13 thoughts on “Harmonyfx – JavaFx Media Player

  1. maaf artikelnya belum selesai, tapi source codenya sudah bisa di download.
    lagu2 itu sebenarnya saya sendiri juga kurang suka kecuali beberapa nasyidnya; tapi karena kebutuhan untuk demo webservice, ngambil album art dari last.fm, jadinya kugunakan beberapa lagu barat yang kemungkinan pasti ada album art-nya di sana.

  2. saya menggunakan netbeans 6.5.1 dan javafx 1.1 SDK
    ketika saya double klik FILE.jar pada dist folder muncul tulisan Main class not found.

    yang ingin saya tanyakan bagaimana caranya merunning java application tanpa menggunakan netbeans atau menggunakan PC lain yang tidak terinstal netbeans.

    Kalau bisa saya minta tutorialnya ya mas..makasih

    • wa’alaikumussalaamwarahmatullah wabarakaatuh,
      link utamnay ya: javafx.com atau ke jfxstudio.org
      terus kalau ada masalah tanya saja ke penulis/kontributor di kedua link di atas.

      usulku, sebaiknya buat yang versi mobile nya, karena pada dasarnya video atau audio sama saja di javafx (codenya gak jauh beda), aplikasi harmonyfx ini di ubah sedikit saja sudah bisa jadi video player lho ;)

      • Wah, terima kasih atas resposnnya yang baik.

        Kira2 kalau modifikasi code punya mas Hakim untuk video player di bagian mananya ya?
        Saya pakai Netbeans 6.7.1 dan javaFX 1.2

        Trus kalo mau download codenya apa ada link downloadnya atau copy paste dari code diatas? maklum newbie ;)

    • Maksudnya pertanyaannnya gini mas. Kalau buat di website gimana konsep database, trus servletnya di javafx Media Player JavaFX ini mas ?? Apa file lagunya disimpan di databasenya, trus pake servlet juga… Trus gmn y… Bingung,, tapi penasaran juga karena katanya JavaFX buat aplikasi dekstop.

      • JavaFx bisa sebagai aplet juga (selain mobile/handphone & TV), masalahnya kalau implementasi seperti yang ini pada drag-dropnya, saya kurang tahu apa javafx aplet bisa drag-drop file dari local file. sedangkan aplikasi HarmonyFx ini pakai bantuan java.

        kalau data audionya di database server, malah lebih gampang lagi. :)

  3. Assalamu’alaikum.
    saya sudah download aplikasinya, trus mau dibuka pakai netbeans 7.1.2 kok gag bisa. projectnya gag di anggap project. padahal netbeansnya udah ada javxfx?. tpi saya pakai linux ubuntu.

Tinggalkan Balasan

Isikan data di bawah atau klik salah satu ikon untuk log in:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s