ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Self-Playing Media with Java Media Framework

by Chris Adamson
10/09/2002

Viewed as a client-installed media player, the Java Media Framework isn't very impressive. It supports only a handful of media types, practically all of which are covered by other applications like Windows Media Player and QuickTime.

But from the content provider's point of view, the story is much more interesting: the fact that JMF is available in an all-Java mode makes it possible to deploy media without a requirement for any particular media technology on the client side -- all that's required is a J2SE Java runtime. For example, tech wiseacre Robert X. Cringely recently announced plans to offer an all-Java MPEG-4 program called "NerdTV" that won't require any client-side player to be pre-installed.

What's more, it's possible to take advantage of some of the abilities of the .jar file format to stuff the decoder and the media into one file, creating, in effect, a "self-playing movie", in much the same way that compression applications like WinZip and StuffIt can create self-expanding archives.

The strategy for this breaks down into three steps:

Source Code:

Download the source file, spmovie-src.tar.gz, for this article.

  1. Enable JMF to play media from inside of a .jar file.
  2. Create a slimmed-down .jar with only the parts of JMF required to play the local media.
  3. Put the code and media in the .jar, and create a suitable manifest to make it double-clickable.

The key to basic JMF playback is to get a Player that's capable of decoding and rendering your media. Typically, this is done by using the Manager to get a suitable DataSource, which in a playback case provides both the media streams and metadata about the stream, such as the media format. The Manager then finds a Player to handle the DataSource. In both cases, the Manager combines a reflection scheme with a list of package prefixes (like javax.media, com.ibm.media, etc.) to try to find suitable classes, throwing exceptions if, for example, a proposed Player can't accept the DataSource offered to it.

The Manager doesn't have much to go on, usually just looking at the protocol and file-name extension in a URL, so while it's easy enough to figure out how to handle file:///Users/cadamson/mymp3stash/some.mp3, it doesn't know what to make of a URL like jar:file:/Users/cadamson/dev/jmftests/spmovie-old/src/gatsbymovie.jar!/movie/themovie.mov.

To alleviate this, we can write a DataSource, or more correctly, a PullDataSource, that takes on the responsibility of figuring things out for the Manager. None of the methods in this JarEntryDataSource is particularly difficult; there are just a lot of them, especially because the PullSourceStream we provide also requires implementations of several super-interfaces.

It's a little lame, but the class depends on filename extensions to return the "content type." This is much like a MIME type, except that it is formatted with periods instead of slashes, so it can be used in package names (e.g., the video/mpeg MIME type becomes video.mpeg so the Manager can find the com.sun.media.codec.video.mpeg package). Here's our simple implementation:

public String getContentType() {
   try {
      URL url = getLocator().getURL();
      String urlFile = url.getFile();
      if (urlFile.endsWith(".mov"))
         return "video.quicktime";
      else if (urlFile.endsWith(".mpg"))
         return "video.mpeg";
      else if (urlFile.endsWith(".avi"))
         // Manager needs '_' insted of '-'
         return "video.x_msvideo";
      else
         return "unknown";
   } catch (MalformedURLException murle) {
      return "unknown";
   }
}

The other annoyance is that the JMF source code (recently pulled from Sun's Web site but expected to return soon) indicates that the default Player can only play a QuickTime DataSource if the provided stream is Seekable, an interface that provides a random-access seek() method. The strategy in the JarEntryDataSource is to use InputStream.skip() if the seek point is further ahead in the stream. If the seek point is behind the current read point (called tellPoint because its value is returned by the Seekable.tell() method), then it must close the InputStream, re-open, and skip to the seek point. It uses an internal thoroughSkip() method to make sure we really end up where we think we are.

public long seek (long position) {
   try {
      if (position > tellPoint) {
         thoroughSkip (position - tellPoint);
      } else {
         close();
         open();
         thoroughSkip (position);
      }
      return tellPoint;
   } catch (IOException ioe) {
      return 0; // bogus...
   }
}

With this class, Manager can find a usable Player for a .mov or .avi file from inside of a .jar file. Our example's TinyPlayer uses the ClassLoader.getResource() to find either movie/themovie.mov or movie/themovie.avi along the classpath. When the classpath only includes a .jar file, we'll be set.

The first step in preparing a suitable .jar file is to use JMF's jmfcustomizer tool to create a jar with just the classes needed to play our media, omitting code for streaming, capture, transcoding, and other functions not needed for a bare-bones player. Unfortunately, Sun doesn't include jmfcustomizer's help file with the all-Java version of JMF, but the various screens of the customizer are easy enough to figure out:

  • Media Source and Media Sink: Select "Media Files" and "Play."

  • Protocol: Just "file."

  • Source Media Formats: "QuickTime (.mov)" and "Avi."

  • Deccoder: Whatever you plan to use in your media, most probably "A-law," "U-law," and/or "IMA4" for audio and "H263" for video. Don't bother with encoders, packetizers, or depacketizers.

  • Renderers: In audio, we want "JavaSound" for Java 1.3 and up, and SunAudio for Sun's pre-1.3 JVMs. For video, "AWT" is all we need.

The result of this is a much slimmer .jar file for our player -- from 1.9 MB in the original jmf.jar to less than 700K for our customized jar.

Assuming you've compiled the two com.mac.invalidname.spmovie classes, add them to the customized jar:

jar uf customized.jar com/mac/invalidname/spmovie/*.class

The license terms of JMF require its read-me file to be distributed with any JMF or customized subset. I've provided this in the misc directories, where TinyPlayer's menu item can find it:

jar uf customized.jar misc/

To make the .jar double-clickable, we provide a manifest file that tells the Java runtime which class inside of the .jar file has a main() to call when double-clicked or launched with the simple -jar command-line argument. The manifest stub also provides a classpath that only includes the jar itself:

Main-Class: com.mac.invalidname.spmovie.TinyPlayer
Class-Path: .

This manifest is added with the command:

jar ufm customized.jar manifest-stub.txt

This file now has all the code for playing a movie inside of the jar. For future use, save it off as spmovie-engine.jar, or something similar.

Related Reading

Java In a Nutshell
By David Flanagan

Now that the engine is set, all we need is media. As you can see from the JMF supported types page, the all-Java version of JMF has a fairly limited set of supported codecs. Probably the best choice for video is H.263, which performs well at a wide variety of bitrates, though it it could be too much for seriously old machines to handle, unless you shrink the video or keep the framerate low. Audio is less cut-and-dried, but I think IMA 4:1 performs reasonably well. Encode or transcode your media appropriately, and copy it to movie/themovie.mov or movie/themovie.avi. Rename the .jar file if you like (I used spmovie.jar) and add the media with:

jar u0f spmovie.jar media/

Note that the "0" is the digit zero, not the letter O; this indicates that we do not want to compress this entry, since the media is already compressed.

The result is the elusive self-playing movie -- a file that knows what class to run when double-clicked, provides all of the code needed to demultiplex, decode, and render a movie, and the movie itself. As an example, here's a small self-playing movie of Playtime for Keagan, my four-month old son (with royalty-free music from FreePlay Music).

It would be a fairly simple matter to extend this concept to an applet, allowing the media to play in any Java-capable browser.

It could be argued that we've solved the wrong problem -- that it is Java virtual machines, not media players, that are in short supply on the client side. But by providing media that is "author once, play anywhere" we've hopefully stayed true to the original goal of Java.

Chris Adamson is an author, editor, and developer specializing in iPhone and Mac.


Return to ONJava.com.