Self-Playing Media with Java Media Framework
by Chris Adamson10/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. |
- Enable JMF to play media from inside of a
.jarfile. - Create a slimmed-down
.jarwith only the parts of JMF required to play the local media. - 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 |
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.