Search This Blog

Loading...

Thursday, 27 March 2014

Expanding JavaFX's media support

Note: For those that don't want to read through the post and just want the patch for MKV support, you can grab it from this ticket, or here if you don't have a JIRA account.

Background

One of the predominant things lacking a "nice" approach in the Java world for years now has been good media support. Oh sure, we had JMF, but anyone who ever had the misfortune of using that will I'm sure understand why that never really took on. (Yes, it really was that bad.) A few other approaches came and went, most notably Java Media Components - but none ever made there way into core Java, and for a long time it became pretty de-facto knowledge that if you wanted any form of comprehensive media support in Java, you used a cross-platform native library, perhaps with a Java wrapper.

However, when JavaFX 2 came along we were provided with a new, baked in media framework that provided this functionality on the Java level! This is a massive step forward, sure it uses GStreamer underneath but that's not really an issue - the required libraries are baked into JavaFX, and the end user can just treat the corresponding MediaView as any other node in the scenegraph.

However, while this is much better than what we had previously, it's rather limited support at this stage:

7. Does JavaFX provide support for audio and video codecs?

JavaFX provides a common set of APIs that make it easy to include media playback within any JavaFX application. The media formats currently supported are the following:
  • Audio: MP3; AIFF containing uncompressed PCM; WAV containing uncompressed PCM; MPEG-4 multimedia container with Advanced Audio Coding (AAC) audio
  • Video: FLV containing VP6 video and MP3 audio; MPEG-4 multimedia container with H.264/AVC (Advanced Video Coding) video compression .

The format support is far from useless; for videos bundled with an application that can be in any format, mp4 with h264 and AAC is certainly a relatively standard option. Likewise, if you're writing an application that uploads videos and uses something like ffmpeg to convert them over to a standard format before displaying them with a JFX frontend somehow, this is also adequate.

However, there are many use cases where the current support is very restrictive indeed - certainly any general purpose media player is out of the question, as is (realistically) any application where you want user-selected video files to play with any degree of reliability. Two of the most common container formats are out whatever formats are inside them (MKV and the badly ageing AVI), AC3 audio support isn't there... well, anything that isn't in the above list isn't there. Which is a lot. Many of these will have been excluded for licensing reasons, though even many free ones that could be in there aren't (MKV, OGG, FLAC, etc.)

But as said already, the JavaFX media classes use GStreamer to do the heavy lifting, which has all these formats (and more) available to it through plugins. So now that the whole thing is open sourced, it should in theory be possible to rebuild JFX with more GStreamer plugins compiled in, right? Turns out it is - the following is an outline of the process of how I did it. I'll be adding support for the MKV container here, other plugins can no doubt be added in a similar way.

Note: I'm far from an authority on this subject, I'm not a JFX developer and I'm certainly not advocating that what I describe here is necessarily all accurate, or correct. It's merely what I've been able to work out from tracing through the source and from some helpful people on the openjfx-dev mailing list.

Setting up the build

You'll firstly need to check out the JFX repo using Mercurial:

hg clone http://hg.openjdk.java.net/openjfx/8u-dev/master/
You will of course need a Mercurial client. If you're using Windows and haven't got a Mercurial client installed already, I highly recommend TortoiseHg. The clone may take a while to complete (the repository is on the larger side!) so be patient.

When you've checked out the repository, you'll then need to make sure you have all the prerequisites you need to build successfully - for that, see the build page and make sure you have everything installed that you need (for Windows users, you must have Cygwin installed with the listed plugins.)

There's a couple of points you also must take note of if you're running Windows that aren't mentioned in the document however:

  • If the DirectX SDK fails to install then it's almost definitely because of the issue described here - uninstall the relevant packages, then try the install again and it should go through without an issue.
  • You must also have the samples from the Windows SDK installed.
  • We want to compile the media module, which is disabled by default for timing reasons. In the root of the repo, copy gradle.properties.template to gradle.properties, then uncomment the line (delete the first hash) that says "#COMPILE_MEDIA = true".
  • Things will be much easier if you add your gradle bin folder to PATH.
You can then fire up a shell, type "gradle sdk" and watch it attempt to build. Since you haven't made any changes at this point, all should go well and you should be presented with a "BUILD SUCCESSFUL" message after a while (the build will take a few minutes to complete.) If not, then go back and double check you've set everything up as per the instructions on the build page. Of course, if you're still stuck then feel free to leave a comment :)

Making the changes

At this point, there's two ways you can proceed - the first is to just grab the patch file, apply it to the repo, and then rebuild (run "gradle sdk" again.) All being well, the MKV container will then be supported in the resulting build. So if you want to just do that the easy way, you can skip the rest of this step.

However, in the interest of being as informative as possible to those that want to repeat the step with another plugin, I'll describe the necessary changes here in detail. Changes on both the Java and native layer are required, so we'll start with the Java layer.

Java layer

The bit of the Java layer that we're interested in is really just responsible for performing some basic checks on the file's type to determine if it has a hope of playing it. This is a relatively simple process, hopefully explained by the diagram below:


This means we have to add support in two places; we have to modify the filenameToContentType() method so it can work out the correct content type from the file name, and we then have to add it to the list of supported content types for the corresponding platform, GSTPlatform in this case (this is just an entry in the array.) Optionally we could also add knowledge of the signature to the fileSignatureToContentType() method, but that isn't strictly necessary since it's just used as a fallback if the type can't be worked out from the fileNameToContentType() method. In doing so you'll also need to add the extension and content type to the MediaUtils class:

public static final String CONTENT_TYPE_MKV = "video/x-matroska";
private static final String FILE_TYPE_MKV = "mkv";

The file extension is obvious, but the content type should be grabbed from the GStreamer defined types list here.

I won't go into huge amounts of detail on what exactly to change in the Java classes - it's pretty basic Java, and I'm assuming most of the people reading this will be Java programmers! You can of course look at the patch to see my exact changes. If you get stuck, feel free to leave a comment and I'll do my best to help.

When you've done this, rebuild JFX (run "gradle sdk") and try creating a media object to point to an MKV file. If the above has worked successfully, you'll get a different (native) error to the one you got before. This is good - it means the Java layer is letting the file pass down to be played in the native layer - now we need to enable it here.

Native Layer

We now need to grab the required plugin for GStreamer - in the case of the matroska plugin, this is in the "plugins-good" category, which means it's a well written and tested plugin that shouldn't pose distribution problems. The tarball of source for the plugins can be grabbed from here.

However, make sure when you're doing this that you grab the correct version of the plugins - JavaFX (for 8u20 and before at least) isn't built with the latest GStreamer. To find out what one, we can look at the GStreamer modifications that Oracle publish, a zip file containing modifications to the bits of GStreamer they've included. This shows the plugins being used are the 0.10.30 ones (for the good branch anyway), so go ahead, download the 0.10.30 good plugins and pull out the matroska one, and drop it in the relevant directory - in this case "modules/media/src/main/native/gstreamer/gstreamer-lite/gst-plugins-good/gst/".

Most of the modifications that Oracle make are fixes and performance improvements to the other plugins, though one required change is to the plugin loading system. In the main plugin C file, matroska.c in this case, you'll see an init function like the following:

GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
    GST_VERSION_MINOR,
    "matroska",
    "Matroska and WebM stream handling",
    plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)

We don't need this function, so we can remove it - instead of removing it though, make sure you follow the convention of wrapping it in "#ifndef":

+#ifndef GSTREAMER_LITE
+GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
+    GST_VERSION_MINOR,
+    "matroska",
+    "Matroska and WebM stream handling",
+    plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)
+#endif

This then makes it much easier to find where changes have been made later on, as well as preserving the original functionality if it's not being built in the JFX environment for whatever reason (in which case GSTREAMER_LITE won't be defined, so the above will execute.)

We also need to change the plugin_init function in a similar way, which is the following:

static gboolean
plugin_init (GstPlugin * plugin)

We want to make two changes here - we don't want it to be static, and we want the name to be more unique so it can be initialised alongside other plugins without any conflict. The convention appears to be "plugin_init_pluginname", so replace the above with the following:

#ifdef GSTREAMER_LITE
gboolean
plugin_init_matroska (GstPlugin * plugin)
#else // GSTREAMER_LITE
static gboolean
plugin_init (GstPlugin * plugin)
#endif // GSTREAMER_LITE

Again, the changes are wrapped in the appropriate tags to make it clear what we've changed.

That's all the changes for this file, but we need to add the method we've defined (plugin_init_matroska in this case) to the appropriate headers file, "modules/media/src/main/native/gstreamer/gstreamer-lite/projects/plugins/gstplugins-lite.h". Open it, and add:

gboolean plugin_init_matroska (GstPlugin * plugin);

...or whatever you called your function above to the list of method headers. We also need to make sure the function is called to initialise the plugin, so open up modules/media/src/main/native/gstreamer/gstreamer-lite/projects/plugins/gstplugins-lite.c and find the section where the plugins are initialised, it'll be a list in a big if statement looking something like this:
...
!plugin_init_aiff(plugin) ||
!plugin_init_app(plugin) ||
!plugin_init_audioparsers(plugin) ||
...
Then just add the call to your plugin in the same way:
...
!plugin_init_aiff(plugin) ||
!plugin_init_app(plugin) ||
!plugin_init_audioparsers(plugin) ||
!plugin_init_matroska(plugin) ||
...
We now need to update the JavaFX cpp bridging code to make it aware of the format, and create and return a pipeline for it. Let's start by defining the format in modules/media/src/main/native/jfxmedia/MediaManagement/MediaTypes.h - this is just a case of adding it to the list, making sure it's the same as you defined it in the Java code. So for this case, the following needs to be added:

#define CONTENT_TYPE_MKV    "video/x-matroska"
Next, open modules/media/src/main/native/jfxmedia/platform/gstreamer/GstPipelineFactory.cpp - this is where the pipeline creation actually takes place. There's a few things that need to be changed in here:
  1. Find the pushback function calls, something like:
         m_ContentTypes.push_back(CONTENT_TYPE_MP4);
         m_ContentTypes.push_back(CONTENT_TYPE_M4A);
         m_ContentTypes.push_back(CONTENT_TYPE_M4V);
    Then add your content type to it in the same way:
         m_ContentTypes.push_back(CONTENT_TYPE_MKV);
  2. The next step is a bit less clear cut, you need to find the CreatePlayerPipeline function, and identify the part where your pipeline should be created. You'll see a general pattern here - the video container formats are dealt with within one if statement that sets up a video sink before creating the pipeline, and the audio formats are dealt with afterwards. So in the appropriate place, you need to follow the pattern to hook in and call a method to create your pipeline. For MKV, it seemed to make most sense to add it to the if/else block just after checking for mp4 files, so straight afterwards I added this:
    
        else if (CONTENT_TYPE_MKV == locator->GetContentType())
     {
      uRetCode = CreateMKVPipeline(pSource, pVideoSink, (CPipelineOptions*) pOptions, ppPipeline);
      if (ERROR_NONE != uRetCode)
                   return uRetCode;
            }
  3. Of course, you then need to write the function to return the pipeline, and again you can follow the pattern of similar ones for this. Once again, MKV is similar in the way it's handled to MP4 so I simply copied that function and made the appropriate changes thus producing:
    1. uint32_t CGstPipelineFactory::CreateMKVPipeline(GstElement* source, GstElement* pVideoSink, CPipelineOptions* pOptions, CPipeline** ppPipeline)
      {
      #if TARGET_OS_WIN32
          return CreateAVPipeline(source, "matroskademux", "dshowwrapper", true, dshowwrapper", pVideoSink, pOptions, ppPipeline);
      #elif TARGET_OS_MAC
          return CreateAVPipeline(source, "matroskademux", "audioconverter", false, avcdecoder", pVideoSink, pOptions, ppPipeline);
      #elif TARGET_OS_LINUX
      #if ENABLE_GST_FFMPEG
          return CreateAVPipeline(source, "matroskademux", "ffdec_aac", true,
                                  "ffdec_h264", pVideoSink, pOptions, ppPipeline);
      #else // ENABLE_GST_FFMPEG
          return CreateAVPipeline(source, "matroskademux", "avaudiodecoder", false, "avvideodecoder", pVideoSink, pOptions, ppPipeline);
      #endif // ENABLE_GST_FFMPEG
      #else
          return ERROR_PLATFORM_UNSUPPORTED;
      #endif // TARGET_OS_WIN32
      }
    The only things I changed from the CreateMP4Pipeline were the name of the function, and the name of the demuxing plugin (matroskademux in this case) - everything else remains the same.
  4. Of course, you'll now need to add the above function to the relevant header file, so in modules/media/src/main/native/jfxmedia/platform/gstreamer/GstPipelineFactory.h, add:
    
    
    uint32_t    CreateMKVPipeline(GstElement* source, GstElement* videosink, CPipelineOptions* pOptions, CPipeline** ppPipeline);
    
    
    ...to the list of functions.
That should be all the changes you need to make to the native code, now we just need to ensure it's compiled and linked properly. So to start with, you'll need to update the plugins makefile, modules/media/src/main/native/gstreamer/projects/win/gstreamer-lite/Makefile.gstplugins - add the directory and all the c files in that appropriate directory. So my list of directories now looks (partly) something like:
...
gst-plugins-good/gst/spectrum/ \
gst-plugins-good/gst/wavparse/ \
gst-plugins-good/gst/matroska/ \
gstreamer/plugins/elements/ \
gstreamer/plugins/indexers/ \
...

And the list of files:

...
gst-plugins-good/gst/spectrum/gstspectrum.c \
gst-plugins-good/gst/wavparse/gstwavparse.c \
gst-plugins-good/gst/matroska/webm-mux.c \
gst-plugins-good/gst/matroska/matroska-parse.c \
gst-plugins-good/gst/matroska/matroska-mux.c \
gst-plugins-good/gst/matroska/matroska-ids.c \
gst-plugins-good/gst/matroska/matroska-demux.c \
gst-plugins-good/gst/matroska/matroska.c \
gst-plugins-good/gst/matroska/lzo.c \
gst-plugins-good/gst/matroska/ebml-write.c \
gst-plugins-good/gst/matroska/ebml-read.c \
gstreamer/plugins/elements/gstcapsfilter.c \
gstreamer/plugins/elements/gstelements.c \
...

The boldings are my addition (obviously the above is just an excerpt.)

Now you can try and build and see if you encounter any compilation errors (you could and arguably should of course, do this as you're going through as well.) I found odd things sometimes cropped up if I didn't do a clean (gradle clean) before re-building, so if something doesn't seem quite right, bear that in mind.

You shouldn't have any compilation errors at this point, but you almost certainly will have linker errors (probably starting with "Unresolved external symbol" or something similar, then referencing a function.) These functions will either start with g_ or gst_ (ignore the leading underscore.) The ones that start with gst need to be added to modules/media/src/main/native/gstreamer/projects/win/gstreamer-lite.def matching the format of the file already there. This means for each function, you need a new line in the format of:

function_name<TAB>@nextsequentialnumber<TAB>NONAME.

Obviously, function_name should be replaced with the function name, <TAB> should be replaced with an actual tab and nextsequentialnumber should be replaced with, you guessed it, the next sequential number. Apart from that you don't need to worry about ordering. For the matroska plugin, I had to add the following to the end of the file:

gst_byte_writer_free_and_get_buffer @184 NONAME
gst_byte_writer_free @185 NONAME
gst_byte_writer_new_with_size @186 NONAME

For the functions that start with "g_" instead, you need to add these in the exact same way to both the modules/media/src/main/native/gstreamer/3rd_party/glib/glib-2.28.8/build/win32/vs100/glib-lite.def and the modules/media/src/main/native/gstreamer/3rd_party/glib/glib-2.28.8/build/win32/vs100/glib-liteD.def files.

If you recompile and it now complains about an unresolved external in one of the def files itself, changes are the source that contains those functions isn't actually getting compiled (because it wasn't needed in any of the plugins already there.) For the Matroska plugin this is the case for the bytewriter functions mentioned above. These are in gstbytewriter.c, and a quick check in the makefile (modules/media/src/main/native/gstreamer/projects/win/gstreamer-lite/Makefile.gstreamer) indeed confirmed that it wasn't on the list, so that was promptly added:

gstreamer/libs/gst/base/gstbasetransform.c \
gstreamer/libs/gst/base/gstbytereader.c \
gstreamer/libs/gst/base/gstbytewriter.c \
gstreamer/libs/gst/base/gstcollectpads.c \
gstreamer/libs/gst/base/gstpushsrc.c \

And that's it - after performing those steps, doing a full build, then running against the built dll's and jfxrt.jar, media support for MKV (or whatever other format you've chosen) should just work!

Summary

These are relatively extensive instructions, but none are really major changes. Summarised, they're pretty much as follows:

  1. Edit the fileNameToContentType() method to return the correct content type for your file name.
  2. Optionally edit the fileSignatureToContentType() method to return the correct type for your file signature (this page may be of use here.)
  3. Add the content type to the list of GSTPlatform's supported content types
  4. Download the plugin file and drop it in the relevant directory
  5. In the plugin's main file, rename the plugin_init function to something more unique, remove the static modifier
  6. Initialise the plugin in gstplugins-lite.c
  7. Define the media type in MediaTypes.h
  8. In GSTPipelineFactory.cpp, call m_ContentTypes.push_back on the content type
  9. In the same file, in the CreatePlayerPipeline function, add in a hook to call a function to create the pipeline for your content type
  10. Create the above function (using the other ones in that file as a template)
  11. Add the relevant files / directory to Makefile.gstplugins
  12. Make sure all the files you rely on are being built, add any that aren't to Makefile.gstreamer
  13. Add any gstreamer functions (gst) to gstreamer-lite.def
  14. Add any glib functions (g) to glib-lite.def and glib-liteD.def
As said already, the above guide is my (little) experience simply with playing around and adding MKV support, so I can't guarantee it will be exactly the same for other plugins and formats. However, it should at least serve as a starting guide for those wanting to build much more comprehensive media support into JavaFX. There's many, many gstreamer plugins available, most of which will, in all likelihood never be included in JavaFX core because of licensing issues. But if you want to build your own version of JFX to distribute with your application, then building much more media support into it, as described above, is more than do-able.

Friday, 31 January 2014

Draggable and detachable tabs in JavaFX 2

JavaFX currently doesn't have the built in ability to change the order of tabs by dragging them, neither does it have the ability to detach tabs into separate windows (like a lot of browsers do these days.) There is a general issue for improving TabPanes filed here, so if you'd like to see this sort of behaviour added in the main JavaFX libraries then go ahead and cast your vote, it would be a very welcome addition!

However, as nice as this would be in the future, it's not here at the moment and it looks highly unlikely it'll be here for Java 8 either. I've seen a few brief attempts at reordering tabs in JavaFX, but very few examples on dragging them and nothing to do with detaching / reattaching them from the pane.

Given this, I've decided to create a reusable class that should hopefully be as easy as possible to integrate into existing applciations - it extends from Tab, and for the most part you create it and use it like a normal tab (you can just add it to a normal TabPane for instance.) It works pretty well for me as you can see in this simple example:



There's a few things to be aware of however before you rush out and use it!
  • To set the text, make sure you use setLabelText() rather than setText(), otherwise you'll get odd results. Sadly the latter is final so I can't override it.
  • You can't have DraggableTabs and normal Tabs on the same TabPane, otherwise you'll see all sorts of errors pop up. If you don't want a particular tab to be detachable, just call setDetachable(false). Tab 1 (the black tab) is set this way in the example program below.
  • This seems to work well for me, but it's far from bulletproof - use at your own risk! It should be pretty easy to work out what's going on though so if you want to change it, modify or otherwise extend it then redistribute it, feel free.


If you don't want to copy / paste from here, just grab the raw files.

/**
 * Just a very simple sample application that uses the class below.
 */
public class FXTabs extends Application {

    @Override
    public void start(final Stage primaryStage) {

        DraggableTab tab1 = new DraggableTab("Tab 1");
        tab1.setClosable(false);
        tab1.setDetachable(false);
        tab1.setContent(new Rectangle(500, 500, Color.BLACK));
        DraggableTab tab2 = new DraggableTab("Tab 2");
        tab2.setClosable(false);
        tab2.setContent(new Rectangle(500, 500, Color.RED));
        DraggableTab tab3 = new DraggableTab("Tab 3");
        tab3.setClosable(false);
        tab3.setContent(new Rectangle(500, 500, Color.BLUE));
        DraggableTab tab4 = new DraggableTab("Tab 4");
        tab4.setClosable(false);
        tab4.setContent(new Rectangle(500, 500, Color.ORANGE));
        TabPane tabs = new TabPane();
        tabs.getTabs().add(tab1);
        tabs.getTabs().add(tab2);
        tabs.getTabs().add(tab3);
        tabs.getTabs().add(tab4);

        StackPane root = new StackPane();
        root.getChildren().add(tabs);

        Scene scene = new Scene(root);

        primaryStage.setScene(scene);
        primaryStage.show();

    }
}


All the real work happens in this class - it's not the neatest thing in the world by a long stretch, but I've kept it all in one place to make it easier to just copy across and experiment with:

import java.util.HashSet;
import java.util.Set;
import javafx.collections.ListChangeListener;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.WindowEvent;

/**
 * A draggable tab that can optionally be detached from its tab pane and shown
 * in a separate window. This can be added to any normal TabPane, however a
 * TabPane with draggable tabs must *only* have DraggableTabs, normal tabs and
 * DrragableTabs mixed will cause issues!
 * <p>
 * @author Michael Berry
 */
public class DraggableTab extends Tab {

    private static final Set<TabPane> tabPanes = new HashSet<>();
    private Label nameLabel;
    private Text dragText;
    private static final Stage markerStage;
    private Stage dragStage;
    private boolean detachable;

    static {
        markerStage = new Stage();
        markerStage.initStyle(StageStyle.UNDECORATED);
        Rectangle dummy = new Rectangle(3, 10, Color.web("#555555"));
        StackPane markerStack = new StackPane();
        markerStack.getChildren().add(dummy);
        markerStage.setScene(new Scene(markerStack));
    }

    /**
     * Create a new draggable tab. This can be added to any normal TabPane,
     * however a TabPane with draggable tabs must *only* have DraggableTabs,
     * normal tabs and DrragableTabs mixed will cause issues!
     * <p>
     * @param text the text to appear on the tag label.
     */
    public DraggableTab(String text) {
        nameLabel = new Label(text);
        setGraphic(nameLabel);
        detachable = true;
        dragStage = new Stage();
        dragStage.initStyle(StageStyle.UNDECORATED);
        StackPane dragStagePane = new StackPane();
        dragStagePane.setStyle("-fx-background-color:#DDDDDD;");
        dragText = new Text(text);
        StackPane.setAlignment(dragText, Pos.CENTER);
        dragStagePane.getChildren().add(dragText);
        dragStage.setScene(new Scene(dragStagePane));
        nameLabel.setOnMouseDragged(new EventHandler<MouseEvent>() {

            @Override
            public void handle(MouseEvent t) {
                dragStage.setWidth(nameLabel.getWidth() + 10);
                dragStage.setHeight(nameLabel.getHeight() + 10);
                dragStage.setX(t.getScreenX());
                dragStage.setY(t.getScreenY());
                dragStage.show();
                Point2D screenPoint = new Point2D(t.getScreenX(), t.getScreenY());
                tabPanes.add(getTabPane());
                InsertData data = getInsertData(screenPoint);
                if(data == null || data.getInsertPane().getTabs().isEmpty()) {
                    markerStage.hide();
                }
                else {
                    int index = data.getIndex();
                    boolean end = false;
                    if(index == data.getInsertPane().getTabs().size()) {
                        end = true;
                        index--;
                    }
                    Rectangle2D rect = getAbsoluteRect(data.getInsertPane().getTabs().get(index));
                    if(end) {
                        markerStage.setX(rect.getMaxX() + 13);
                    }
                    else {
                        markerStage.setX(rect.getMinX());
                    }
                    markerStage.setY(rect.getMaxY() + 10);
                    markerStage.show();
                }
            }
        });
        nameLabel.setOnMouseReleased(new EventHandler<MouseEvent>() {

            @Override
            public void handle(MouseEvent t) {
                markerStage.hide();
                dragStage.hide();
                if(!t.isStillSincePress()) {
                    Point2D screenPoint = new Point2D(t.getScreenX(), t.getScreenY());
                    TabPane oldTabPane = getTabPane();
                    int oldIndex = oldTabPane.getTabs().indexOf(DraggableTab.this);
                    tabPanes.add(oldTabPane);
                    InsertData insertData = getInsertData(screenPoint);
                    if(insertData != null) {
                        int addIndex = insertData.getIndex();
                        if(oldTabPane == insertData.getInsertPane() && oldTabPane.getTabs().size() == 1) {
                            return;
                        }
                        oldTabPane.getTabs().remove(DraggableTab.this);
                        if(oldIndex < addIndex && oldTabPane == insertData.getInsertPane()) {
                            addIndex--;
                        }
                        if(addIndex > insertData.getInsertPane().getTabs().size()) {
                            addIndex = insertData.getInsertPane().getTabs().size();
                        }
                        insertData.getInsertPane().getTabs().add(addIndex, DraggableTab.this);
                        insertData.getInsertPane().selectionModelProperty().get().select(addIndex);
                        return;
                    }
                    if(!detachable) {
                        return;
                    }
                    final Stage newStage = new Stage();
                    final TabPane pane = new TabPane();
                    tabPanes.add(pane);
                    newStage.setOnHiding(new EventHandler<WindowEvent>() {

                        @Override
                        public void handle(WindowEvent t) {
                            tabPanes.remove(pane);
                        }
                    });
                    getTabPane().getTabs().remove(DraggableTab.this);
                    pane.getTabs().add(DraggableTab.this);
                    pane.getTabs().addListener(new ListChangeListener<Tab>() {

                        @Override
                        public void onChanged(ListChangeListener.Change<? extends Tab> change) {
                            if(pane.getTabs().isEmpty()) {
                                newStage.hide();
                            }
                        }
                    });
                    newStage.setScene(new Scene(pane));
                    newStage.initStyle(StageStyle.UTILITY);
                    newStage.setX(t.getScreenX());
                    newStage.setY(t.getScreenY());
                    newStage.show();
                    pane.requestLayout();
                    pane.requestFocus();
                }
            }

        });
    }

    /**
     * Set whether it's possible to detach the tab from its pane and move it to
     * another pane or another window. Defaults to true.
     * <p>
     * @param detachable true if the tab should be detachable, false otherwise.
     */
    public void setDetachable(boolean detachable) {
        this.detachable = detachable;
    }

    /**
     * Set the label text on this draggable tab. This must be used instead of
     * setText() to set the label, otherwise weird side effects will result!
     * <p>
     * @param text the label text for this tab.
     */
    public void setLabelText(String text) {
        nameLabel.setText(text);
        dragText.setText(text);
    }

    private InsertData getInsertData(Point2D screenPoint) {
        for(TabPane tabPane : tabPanes) {
            Rectangle2D tabAbsolute = getAbsoluteRect(tabPane);
            if(tabAbsolute.contains(screenPoint)) {
                int tabInsertIndex = 0;
                if(!tabPane.getTabs().isEmpty()) {
                    Rectangle2D firstTabRect = getAbsoluteRect(tabPane.getTabs().get(0));
                    if(firstTabRect.getMaxY()+60 < screenPoint.getY() || firstTabRect.getMinY() > screenPoint.getY()) {
                        return null;
                    }
                    Rectangle2D lastTabRect = getAbsoluteRect(tabPane.getTabs().get(tabPane.getTabs().size() - 1));
                    if(screenPoint.getX() < (firstTabRect.getMinX() + firstTabRect.getWidth() / 2)) {
                        tabInsertIndex = 0;
                    }
                    else if(screenPoint.getX() > (lastTabRect.getMaxX() - lastTabRect.getWidth() / 2)) {
                        tabInsertIndex = tabPane.getTabs().size();
                    }
                    else {
                        for(int i = 0; i < tabPane.getTabs().size() - 1; i++) {
                            Tab leftTab = tabPane.getTabs().get(i);
                            Tab rightTab = tabPane.getTabs().get(i + 1);
                            if(leftTab instanceof DraggableTab && rightTab instanceof DraggableTab) {
                                Rectangle2D leftTabRect = getAbsoluteRect(leftTab);
                                Rectangle2D rightTabRect = getAbsoluteRect(rightTab);
                                if(betweenX(leftTabRect, rightTabRect, screenPoint.getX())) {
                                    tabInsertIndex = i + 1;
                                    break;
                                }
                            }
                        }
                    }
                }
                return new InsertData(tabInsertIndex, tabPane);
            }
        }
        return null;
    }

    private Rectangle2D getAbsoluteRect(Control node) {
        return new Rectangle2D(node.localToScene(node.getLayoutBounds().getMinX(), node.getLayoutBounds().getMinY()).getX() + node.getScene().getWindow().getX(),
                node.localToScene(node.getLayoutBounds().getMinX(), node.getLayoutBounds().getMinY()).getY() + node.getScene().getWindow().getY(),
                node.getWidth(),
                node.getHeight());
    }

    private Rectangle2D getAbsoluteRect(Tab tab) {
        Control node = ((DraggableTab) tab).getLabel();
        return getAbsoluteRect(node);
    }

    private Label getLabel() {
        return nameLabel;
    }

    private boolean betweenX(Rectangle2D r1, Rectangle2D r2, double xPoint) {
        double lowerBound = r1.getMinX() + r1.getWidth() / 2;
        double upperBound = r2.getMaxX() - r2.getWidth() / 2;
        return xPoint >= lowerBound && xPoint <= upperBound;
    }

    private static class InsertData {

        private final int index;
        private final TabPane insertPane;

        public InsertData(int index, TabPane insertPane) {
            this.index = index;
            this.insertPane = insertPane;
        }

        public int getIndex() {
            return index;
        }

        public TabPane getInsertPane() {
            return insertPane;
        }

    }
}

Saturday, 13 April 2013

The comprehensive (and free) DVD / Blu-ray ripping Guide!

Note: If you've read this guide already (or when you've read it) then going through all of it each time you want to rip something can be a bit of a pain, especially when you just need your memory jogging on one particular section. Because of that, I've put together a quick "cheat sheet" here which acts as a handy reference just to jog your memory on each key step.

I've seen a few guides around on ripping DVDs, but fewer for Blu-rays, and many miss what I believe are important steps (such as ensuring the correct foreign language subtitles are preserved!) While ripping your entire DVD collection would have seemed insane due to storage requirements even a few years ago, these days it can make perfect sense.

This guide doesn't show you a one click approach that does all the work for you, it's much more of a manual process. But the benefits of putting a bit more effort in really do pay off - you get to use entirely free tools with no demo versions, it's reliable and works with all discs I've tried it with (bar one that was heavily scratched!), you get much more control over the quality / file size ratio and quality / encode time ratio, you can ensure any forced subtitles are correctly transferred across - the list goes on. You can also be sure that you get a high quality, completely DRM-free rip that you can do with as you choose without fear that some built in time bomb is going to stop the file from ever playing again if you transfer it to another computer.

Usual disclaimer that this isn't necessarily legal in your country, so check first and proceed with caution - I present this as a technical guide on the process only. If you are going to do it, just make a single personal copy.

Tools

I'll detail the tools used as we go, but it may make sense to install them ahead of time so you can follow the article more easily.

MakeMKV - We'll be using this to rip the raw files from the disc, this works with both Blu-rays and DVDs. If MakeMKV complains about not having a license key, grab the latest one from here.
Handbrake - We'll be using this to compress the file that MKV generates down to a more reasonable size.
VLC - We'll be using this to play / check the MKV files. Any media player that can play them will do, but if you're not sure grab this since it definitely does.

MKVExtract - We'll be using this to extract subtitle streams.
Subtitle EditWe'll be using this to convert subtitles into a better format.


The Initial Rip

Put your disc in the drive and load up MakeMKV (get rip of any auto-playing DVD players that might pop up.) You should be presented with something like the following:



Select the correct optical drive from the list if you have more than one (unlikely these days) and then hit the big button. MakeMKV will then proceed to analyse your disc, which may well take a while - when it's done it'll show you the titles, like so:


There may be as few as one title, or tens of titles depending on the disc. If your DVD contains a film, then you want to select the title with the largest size (in this case that's the first one, 6.2GB.) If it's a TV show you're ripping, you should have selected one title for each episode, and they should all be around the same size (ish.) The other titles are likely extras we don't care about, so we'll deselect those. If there's any titles you're unsure about, just include them anyway - it'll take a bit longer to rip, but that's better than doing the whole disc again! 

Expand the view for your chosen title, and you'll likely see something like this:


Each title contains various tracks, and it's these tracks that you're seeing here - the video track is the one at the top (which you definitely want!) there's audio tracks in different languages, and various subtitle tracks. However, I'm not interested in most of these, so I can deselect them. Presuming you're English go ahead and deselect all apart from the audio track that says "English" and the subtitles tracks that say "English" (do this even if you don't care about subtitles, I'll explain later, honest!) Careful if there's more than one audio track that says English that you select the right one (there may be a couple of others, for instance directors commentaries or audio subtitles. The one you want is usually the first "English" one on the list, and usually the one with the highest quality audio (it's often the only one to have surround sound for instance.)

For Blu-rays the view looks slightly different but essentially the same:

The thing to make sure of here is that you select the "DTS-HD Lossless English" audio as well as the normal "DTS 3/2+1" subitem, to make sure you copy all the uncompressed audio across from the disc. You also have a better option with subtitles, in that you can just tick "English (forced only)" as I have here, rather than checking all the tracks.

Select an output folder for the files (I usually make a temporary folder on the desktop to store them in) then hit the Make MKV button, and wait! It has to pull all the data off the disc, so will take quite a while. On my machine it's usually around 20 minutes for a DVD, up to an hour for a Blu-ray. Yours may be longer though, so be patient!

Test the MKV files

When MakeMKV is finished, open each file in VLC and check it's what you think it is - you can then delete any MKV files from titles you don't want, and if you like rename any MKV files so you know what they are more easily (episode 1, episode 2) for instance.

You should now have a rip of whatever DVD or Blu-ray you chose, congratulations! However, take a look at the file size - it's likely huge (it may be around 6 or 7GB for a DVD, and around 30GB for a Blu-ray.) Rip your whole collection and leave it like that, and unless you have access to a colossal amount of storage, you'll run out of space rather quickly! You may also not want an MKV file, you may want something to play on your ipod or similar. The next steps will involve reducing the file size down to around 1/3 of what it is at the moment with no noticeable loss in quality (there are various tradeoffs in this sense over file size and quality, we'll get to those later.)

Checking the subtitles

This is important! This is the part that most guides miss, but can unknowingly wreck many films if you miss this bit out! What we'll do here is check whether the film has any forced subtitles, and if so translate them into a better format.

Forced subtitles are those that appear even when subtitles are normally disabled, and usually contain captions to translate foreign languages spoken by characters in a film. If you're sure that your chosen film or TV show contains no such captions then you can skip this step, but if you're not sure then follow it anyway to check, otherwise your rip may have large parts you can't understand! This page gives a rough idea of some films that have forced subs, but it's by no means comprehensive.

When MakeMKV is finished, you should have a folder with an MKV file in for each title (for films, this will likely just be one MKV.) Fire up the MKVExtract GUI, and drag the MKV file onto it. You'll get a screen that looks a bit like this:


Select each track that says subtitles, make sure "use source dir for output" is checked and then hit "Extract." (If you hit "English (force only)" and had a Blu-ray disc, and there's no subtitle tracks here, that's fine - it's because there's no forced subtitles and you can skip this step.) When it's done, you should then have a .idx and a .sub file (just a .sup file if you're doing this for a blu-ray) for each subtitle track in the same folder as the mkv file. Examine the size of these files (ignore any idx files for this step.) Take the largest file, which will probably be the full subtitles for the film. Now have a look at the other sub / sup files, is there any one that's significantly less (less than half the size) of the others? If there is, you've likely found your forced subtitle track, and this is the one you'll want to include in your final rip. If not and the files are all around the same size, then you can likely skip the rest of this step and continue.

However, there are times when the forced subtitles are mixed in the same track as the normal subtitles (rare, the only discs I've found that do this thus far are the Game of Thrones DVDs), so if that's the case you'll need to export the entire file and just save the forced subs to the srt file (covered later on.)

This is a typical example that does have a forced subtitle track from an episode of Game of Thrones (Blu-ray):

We can clearly see two sup files, one around 16MB and the other around 44KB. It's the late 44KB one that's the forced track in this case, used when the Dothraki are speaking (Game of Thrones viewers will know what I mean!) This is a particularly extreme example - the forced subtitle track only has two lines, so in many cases it may be larger (but in almost all cases, significantly smaller than the main subtitle track.)

If you used a Blu-ray and clicked the forced only option, you can bypass this check - if there is a sup file there then you have your forced track in front of you. If not, there were no forced subtitles you need to worry about, and you can skip this step.

Of course, I'm assuming here that you don't want the full subtitle track; you may well do, for instance if the film is in a foreign language throughout - in this case, do the following for each subtitle track you want in your final rip.

You could just leave it at that, but here (this is the comprehensive guide, after all!) we're going to convert to SRT format. SRT subtitles use text as the format rather than an image, which gives a number of advantages - it takes up much less space than an sub / sup track, you can customise the colour / font / size of the subtitles trivially within the application that's playing them, you can trivially make any corrections if there's spelling mistakes (has been known to happen!) and they will be rendered smoothly on any resolution screen. In the case of DVDs, SRT files also look much better on any modern monitor or television, since they're rendered in the native resolution rather than displayed in scaled up, blocky 704x576.

Load up Subtitle Edit, and open the sub or sup file. You'll be presented with a screen similar to this:



Hit the "Start OCR" button, then subtitle edit will go through and OCR the entire subtitle file. For most foreign language subtitles this won't take long at all - you'll see it as it goes through, and if it gets stuck it'll prompt you to check that it's got it right (or provide a correction if it hasn't.)

When done, you'll be taken to the main subtitle edit window with the subtitles displayed. All you need to do now is save it (File -> Save), - ".srt" should be selected as the file type by default, but if not, change it so it's definitely in srt format rather than another type.

Repeat this process for each MKV file you have - in the case of TV shows, this may be a few per disc. When you've got more than one title to do, make sure you know what subtitle file goes with what episode!

Transcoding / Compressing

 Almost there - just one more step! Open up handbrake and drag your MKV file onto it (ignore any warning that appears about automatically named output files.)

If you have a subtitles track (srt file as created above), click on the subtitles tab, click import SRT, and locate your srt file you saved earlier. Make sure the "Default" checkbox is selected (important!), and you should end up with something like this:




When you've sorted the subtitles out, click the "Browse" button and select your destination location.

Don't click on "Start" just yet. You could, and you'd end up with a perfectly playable MKV or MP4 file on handbrake's default settings, but it may not be what you want.

Are you ripping this specifically for a device? If so, click on the preset on the right hand side and then click Start. These should encode relatively quickly, if you're unhappy with the result then just re-encode with the Video Quality slider set a bit higher - find it on the video tab (set it to something like 19 instead of 20 - yes, a lower number is better quality!)

However, if you're just ripping it to have as part of a media centre (which is what I do) then I'd recommend my process (see Note 2), which is adjusting the settings from Handbrake's default to the following:
  • Head over to the "Video" tab, and set the slider value there to 19. (I notice some blocking especially on fast action films when you leave it at 20, but setting it to 18 drastically increases file size. You may wish to experiment to find what works best for you, your eyes may be better or worse than mine!)
  • If you want to preserve surround sound (I do) then head over to the "Audio" tab, and under the "Codec" drop down option for the track, select Auto-passthru. You should end up with something like this:

This is important if you want surround sound - if you leave it at the default option it'll be downmixed to a reduced quality stereo. I only have a stereo setup of speakers at the moment, but I do this because if I did get a surround sound system, re-encoding every single DVD just so I can have surround sound would be a nightmare - better to do it right the first time! I'd suggest you do the same, unless you know you really definitely will never need surround sound in your rips and/or you want your rips to be as small as possible.
  • Select "MKV file" as the container - it's technically much more flexible and generally better than MP4, and all good media players now support it. (See Note 1 if you're after avi.)
  • Head back to the video tab, and under the "Optimise Video" group, drag the x264 preset slider until it gets to "Very Slow":


Then you can hit "Start", and wait for your video to encode!

When Handbrake is done, then there you have it - your very own DVD or Blu-ray rip! Just to make sure everything went smoothly though, it's best to double check things worked ok.


Check the subtitles!

Yup, we're back with subtitles again, though this time it's just a precautionary measure - check that any forced subtitles you're expecting to show up, actually do show up. There seems to be a bug in Handbrake with subtitles that means under certain conditions (for me this happens often with Blu-rays but not with DVDs) they won't always get written in properly.

If this happens, then fire up MKVMerge (see "joining MKV files" for a description) and load in your transcoded file, deselect the text stream that's in the file, then click "add" and add in your SRT file. MKVMerge (at least under all conditions I've tested it with) seems to do this correctly every time, unlike Handbrake.



Joining MKV files

Most of the time you won't need this step at all - it only applies to a few films I know of (such as the Lord of the Rings extended editions.) But this is where ripping can really have an advantage, because there's no need to swap discs over half way through, or even MKV files - you can rip the discs separately and then join them together in one continuous file. To do this, you need to rip both discs with *exactly* the same settings all round in Handbrake (so if you follow the above guide to the letter you should be fine) and then use a tool called MKVMerge. (This is a fantastic tool that also lets you add things like extra subtitle and audio streams after you've ripped the file, but here we'll be using it to join two MKV files together.)

MKVMerge is part of the MKVToolnix suite, which you can grab from here. When it's installed, you want to run the "MKVMerge GUI".

Once you've got it running, appending the two files together is simple and doesn't take long at all. Just click the add button, then select the first MKV file, then click append (Note: that's append the second time not add!) and select the second MKV file. You should end up with something that looks like this:



Select the output filename (towards the bottom of the window), then click on "Start muxing" and the program will join the two files seamlessly. If it spits back any errors, it's probably becase the two files weren't encoded with exactly the same settings - so double check and try again.


Wrap-up

This may have seemed like an incredibly long process for ripping a disc, and some will say it's overkill. However, while it might take a while the first time, after ripping a few discs this way the process sped up significantly. When you get used to what to do, it really doesn't take long at all, and the results can be anything from a convenient ipod-playable file to a high quality home media centre setup where you can search through and browse available films using something like XBMC. Setting that up isn't too complicated in itself either - but that's for another post!


-------------------------------------------------------------------

* Note 1: A lot of people seem to ask "Where's AVI?!" at this point, and get very upset when they can't find it. Let go! Seriously, don't bother with it, it's a horrible antiquated format that can't hold separate tracks, can't support modern codecs and can't support any files larger than 2GB (so it's absolutely useless for Blu-ray encodes especially, and very very limited even for DVD rips.) It may be what you're used to, but these days it's really awful. Don't go looking for it, Handbrake doesn't support it any more! You can of course select MP4 if you need it (usually if the device you're playing it on doesn't support MKV.)

* Note 2: How you set up handbrake is very much a personal choice, there's no right answer so don't be afraid to experiment around with the options. I've detailed there the options that I use, including the "Very Slow" preset. This deliberately takes a long time to do the encode, but produces (pretty much) the best quality / filesize ratio. There is one beyond this - placebo. The placebo preset takes this to extremes, but because of diminishing returns, it's really not worth it at all. You might have a 0.000001% increase in filesize : quality ratio if you're lucky, and the encode time will go from a few hours to a few days, hogging up 100% of CPU for all that time and having to be resumed from square one if anything goes wrong or your computer decides to run updates and restart...! You may wish to go for a faster encode if you want results more quickly, but "Very Slow" is really the slowest practically useful encode.

Tuesday, 5 March 2013

Annoyingly named methods

There's several annoyances I have with method names in the Java API - some are well documented as being ridiculous early design decisions that, well, couldn't be reverted without breaking backwards compatibility. Boolean.getBoolean() is a classic example for instance that really doesn't do what you'd expect.

However, there's another which is more recent that I haven't really heard complained about anywhere else. It's not a deal breaker by any stretch of the imagination, but it is a bad name, and it is mildly annoying.

I'm talking about the removeAll() method present on collection classes. It takes a collection of items to remove from that particular collection - pretty standard stuff. But to me, that should be called removeEach(),  removeAllOf(), or perhaps even just remove(), overloaded with the standard method that takes one parameter (to me the latter seems like the most logical choice.)

The problem I have with removeAll() is that it sounds like a method that you would call to do just that - remove all items from the collection. Wipe it. Now this isn't too bad, because if you get it wrong and call it with no arguments, it doesn't compile, and you can fix it pretty easily.

With JavaFX however, they've very helpfully added a var-args method with this name, that does the same thing. This may seem like a logical extension, but because it's a var-args method, passing no parameters, which does nothing, is a perfectly valid option. Not just at compile time, but at runtime as well. It all goes fine, apart from the method does absolutely nothing. It fails silently.

Perhaps a better implementation would have been to make it a two argument constructor, a "normal" reference to a parameter of type E, and then a var-arg list afterwards, essentially making it a "one or more" var-arg list. Granted, this would remove the ability to directly pass an array in - but an overloaded method expecting an array of that type could easily be added to get around that issue.

Anyway, rant over. Back to playing around with cubic Beizer curves for me!

Tuesday, 11 December 2012

JavaFX - some later thoughts

I've been using JavaFX for a while now, in a couple of projects - for my PhD work, and on the side for Quelea (whose interface has been migrated entirely from Swing over to JavaFX.) As promised, I thought I'd post a few follow up thoughts - good points / bad points now that I've been using it a while.

So, without further ado, the positives!

  • The API is nice and clean - and not just because it's not stuffed with deprecated methods like Swing is, it's just designed in a fundamentally much more sensible way which makes it easier to follow. The concept of properties on elements, with these properties having a common interface, means I can jump right in with a component and see what properties are available to me without having to dig in the documentation to find out exactly how to add a listener for the width of the bottom half of my split pane (for instance.)
  • Layout is vastly improved - a lot of the swing inconsistencies have gone, and the model is now a specific component is tied to a specific layout (HBox, VBox, GridPane and so on) rather than having a separate content pane which can have a layout applied to it. This again ties in with the first point, making for a much easier, nicer API without having to wonder exactly what, if any parameters I need to shove on the end to get the layout manager which I've selected (and what one was it, anyway?) to behave.
  • You can actually do animations with keyframes without resorting to horrible graphics2d hacks - and it's all GPU accelerated.
  • Multimedia support comes as standard, no need to play around with the buggy as hell Javasound or JMF.
  • UI work on the platform thread is enforced most of the time - a runtime exception is thrown if you don't do this, which makes it much easier to find and solve odd annoying concurrency bugs that would often crop up otherwise.
  • The default cross platform skin doesn't make me want to vomit. On the contrary, it actually does a good job of looking rather nice across a range of platforms. It's visually appealing and nicely animated too.
  • Nice native deployment options - they only work on the current platform (i.e. you can't build a deb package on a windows box) but still, I like the change of thought that users are generally much more comfortable with a custom built package for their OS rather than a generic jar / jnlp file.
Despite the above, it's not flawless:
  • The multimedia support is great - when it works. But it supports in reality a very limited range of formats and file types (no mkv at all for instance, some of my mp4 encoded videos still didn't work either.)
  • There's still a few annoying bugs I've come across, such as this one. Nothing that can't be sorted, but these sorts of things are annoying.
  • Some features just aren't there yet - I wanted a pop up panel similar to the ColorPane, but those components just don't exist yet (or at least aren't part of the public API.)
  • No rich text on controls, at least not yet. This gets really annoying if you want to (for instance) bold part of a label. Can't be done, you have to butt multiple labels together in a HBox to get that effect (which, let's face it, is nasty.)
  • No native skins - you can write your own, but at the moment (as far as I know) none are provided to make it look like the native platform you're working on. Some would argue this is a good thing, but sometimes it's nice to have this option.
  • No damn font metrics in the public API - again, something I have to use an internal class for at present (which is really rather annoying and means code I write at present potentially isn't backwards compatible.)
Overall, I must admit I still like it, and while I've uncovered more things I don't like from my initial positive reaction, most of those things are slated for inclusion in Java 8 (so in a year or so at the time of writing.) I still don't think it's going to take off on the mobile or web front - but as a replacement for Swing on the desktop, it's a very welcome (and arguably long overdue) change.

Thursday, 16 February 2012

Dropbox Java API

As well as being useful as general cloud storage, dropbox also has an API that lets you access its contents programmatically. It's a straightforward REST API with a number of language specific libraries to make the going a bit easier. Java is included on the list of SDKs, but at present only Android is included on the list of tutorials. This can be somewhat frustrating because simple examples using Java are lacking.

Fortunately, the process was described by Josh here. So I've taken it and implemented it in Java, and it seems to work. It's a very basic example that authenticates with dropbox then uploads a file called "testing.txt" containing "hello world."

Of course, more functionality is available than this, but this is the hard part (at least I found working this bit out the hard part.) Once you've got your DropboxAPI object, you can work most things from there using the supplied Javadoc.

/**
 * A very basic dropbox example.
 * @author mjrb5
 */
public class DropboxTest {

    private static final String APP_KEY = "APP KEY";
    private static final String APP_SECRET = "SECRET KEY";
    private static final AccessType ACCESS_TYPE = AccessType.APP_FOLDER;
    private static DropboxAPI<WebAuthSession> mDBApi;

    public static void main(String[] args) throws Exception {
        AppKeyPair appKeys = new AppKeyPair(APP_KEY, APP_SECRET);
        WebAuthSession session = new WebAuthSession(appKeys, ACCESS_TYPE);
        WebAuthInfo authInfo = session.getAuthInfo();

        RequestTokenPair pair = authInfo.requestTokenPair;
        String url = authInfo.url;

        Desktop.getDesktop().browse(new URL(url).toURI());
        JOptionPane.showMessageDialog(null, "Press ok to continue once you have authenticated.");
        session.retrieveWebAccessToken(pair);

        AccessTokenPair tokens = session.getAccessTokenPair();
        System.out.println("Use this token pair in future so you don't have to re-authenticate each time:");
        System.out.println("Key token: " + tokens.key);
        System.out.println("Secret token: " + tokens.secret);

        mDBApi = new DropboxAPI<WebAuthSession>(session);
        System.out.println();
        System.out.print("Uploading file...");
        String fileContents = "Hello World!";
        ByteArrayInputStream inputStream = new ByteArrayInputStream(fileContents.getBytes());
        Entry newEntry = mDBApi.putFile("/testing.txt", inputStream, fileContents.length(), null, null);
        System.out.println("Done. \nRevision of file: " + newEntry.rev);
        
    }
}

To use the token pair in future, instead of doing:
WebAuthSession session = new WebAuthSession(appKeys, ACCESS_TYPE);

You would do:
WebAuthSession session = new WebAuthSession(appKeys, ACCESS_TYPE, new AccessTokenPair(key, secret));

...where key and secret are the ones printed in the example above. You can then skip the rest of the auth code (up until you create the DropboxAPI object.)

Wednesday, 15 February 2012

Sometimes, older is better...

When I first started Quelea, I wanted to use it as a kind of testing ground for features and ideas that hadn't already been incorporated into church presentation software. Both in terms of the UI, features and functionality. A lot of these features have worked really well - the ability to import from survivor songbooks for instance, and the instant search that's further improved in the next release.

On the UI side of things I tried to have a go with the ribbon - it's still got a love/hate relationship with people, but I wondered if it could do any good in Quelea. So I had a go, implemented it and left it as such for a few releases.

Thing is, it just didn't work - and I think this was a 50:50 split between it not being suitable for Quelea and the flamingo implementation.

On the suitable for Quelea side:
  • For something that's often run on laptops with small screens, it took up a huge amount of space it didn't need to.
  • There weren't enough controls to make it viable. It works (ish) for office because it replaced a hugely complex menu system, but Quelea just doesn't have that complex menu system, and it won't for the foreseeable future. So it really just acted like a huge toolbar.
And on the flamingo side:
  • It was pretty unmaintained, which doesn't exactly add to my confidence.
  • It only integrated well with the substance look and feel, which users might not want to use.
  • It only integrated at all well with Windows, and Quelea is targetted at cross platform use.
  • It looked - well, odd. Most bits are there, but odd bits like the tight integration with the Window aren't, and that just makes it feel like something's not quite there with it. Combine this with the first point and it isn't going away any time soon either.
Perhaps there are some things I could've chosen to make it look better - like designing the UI in SWT rather than Swing (which has a much nicer looking ribbon.) But I've now replaced the ribbon with a standard set of toolbars and menus, and personally I think this looks much nicer. We'll see how it pans out in practice, but sometimes the older, traditional way is definitely the better one!