QuickTime for Java: A Developer's Notebook/Playing Movies

From WikiContent

< QuickTime for Java: A Developer's Notebook(Difference between revisions)
Jump to: navigation, search
(Initial conversion from Docbook)
Current revision (13:22, 7 March 2008) (edit) (undo)
(Initial conversion from Docbook)
 
(2 intermediate revisions not shown.)

Current revision

QuickTime for Java: A Developer's Notebook

Even if you have more elaborate plans for QuickTime for Java, I'm going to assume that your plans will, at some point in time, require reading in a movie or other QuickTime-compatible file, locally or from the network. This chapter presents basic techniques of getting a Movie object, getting it into the Java display space, and adding more sophisticated controls so that your user (or just your code) can know what's happening inside a playing movie and take control.

Contents

Building a Simple Movie Player

I'll begin with "the simplest thing that could possibly work:" an application to ask the user for the location of a QuickTime file, which is then opened and put in a Java AWT Frame.

How do I do that?

Example 2-1 shows the code for a simple movie player.

Example 2-1. Simple movie player

package com.oreilly.qtjnotebook.ch02;
 
import quicktime.*;
import quicktime.app.view.*;
import quicktime.std.movies.*;
import quicktime.io.*;
 
import com.oreilly.qtjnotebook.ch01.QTSessionCheck;
 
import java.awt.*;
 
public class BasicQTPlayer extends Frame {
  public BasicQTPlayer (Movie m) throws QTException {
      super ("Basic QT Player");
      QTComponent qc = QTFactory.makeQTComponent (m);
      Component c = qc.asComponent( );
      add (c);
  }
 
  public static void main (String[  ] args) {
      try {
          QTSessionCheck.check( );
          QTFile file =
              QTFile.standardGetFilePreview (
          QTFile.kStandardQTFileTypes);
          OpenMovieFile omFile = OpenMovieFile.asRead (file);
          Movie m = Movie.fromFile (omFile);
          Frame f = new BasicQTPlayer (m);
          f.pack( );
          f.setVisible(true);
          m.start( );
      } catch (Exception e) {
          e.printStackTrace( );
      }
  }
}

Compile this from the command line (remember, as shown in the previous chapter, you must specify QTJava.zip in the classpath; this is the Mac OS X version):

Note

If you've downloaded the book code, compile and run with ant run-ch02-basicqtplayer.

cadamson% javac -d classes -classpath 
  src:/System/Library/Java/Extensions/QTJava.zip 
  src/com/oreilly/qtjnotebook/ch02/BasicQTPlayer.java

Then run the program from the command line:

cadamson% java -classpath classes
  com.oreilly.qtjnotebook.ch02.BasicQTPlayer

When the program starts up, the user will initially see QuickTime's file selector, shown in Figure 2-1.

Figure 2-1. QuickTime file selector

QuickTime file selector

After the user selects a file (note that I have not provided any error handling if the user clicks Cancel!), the movie will open in a window at its default size and start playing, as seen in Figure 2-2.

Figure 2-2. Simple movie player

Simple movie player

Note that this program does not provide any means of quitting the application once the movie finishes playing (or before then, for that matter). Press Ctrl-C from the command line to kill the app. Mac users will also get a "Quit com.oreilly.qtjnotebook.ch02.BasicQTPlayer" menu item.

What just happened?

Take a look at the application. The class extends java.awt.Frame and supplies a constructor that takes a quicktime.std.movies.Movie object. Given this Movie , it asks the QTFactory (in package quicktime.app.view) for a QTComponent. From this object, it gets a java.awt.Component , which is added to the Frame.

The main( ) method starts by doing the QuickTime session check from the last chapter. Then it brings up a file selector dialog, from which it gets a quicktime.io.QTFile , from which it gets an OpenMovieFile, which leads to the creation of a Movie object with Movie.fromFile( ) . This Movie is then passed to the QTBasicPlayer constructor, and the resulting Frame is pack( )ed and shown. Finally, main( ) calls the Movie's start() method to play the movie.

Notice how practically every line of code in this application either declares that it throws QTException or is wrapped in a try-catch block. That's because pretty much every QuickTime Java call can potentially throw a QTException, which means you either need to catch it or (more frequently) declare that your method throws it to the caller. Presumably at some point further up the call chain, you'll catch the exception and do something responsible with it, such as bringing up an error dialog.

Another point of interest is the QTComponent . This is an interface that exposes methods that allow you to change the movie (or image) displayed by an on-screen widget. asComponent() returns an AWT Component that can be added to an AWT layout just like any other component. Now here's the dirty little secret: all QTComponents received from the QTFactory really are AWT Components, and can be cast safely. That means the asComponent( ) call:

Component c = qc.asComponent( );

is functionally equivalent to:

Component c = (Component) qc;

Meaning that asComponent( ) is really there just for compile-time type safety.

What about...

...using the AWT or Swing file selector? Sure, you can use these—they'll return a java.io.File object, which can then be used to get a QTFile. But the QuickTime file selector is arguably nicer, because on Windows it shows a little preview of the selected movie. Another thing to notice is the odd little constant kStandardQTFileTypes. The standardGetFilePreview( ) call takes an int[ ] of up to four "types" of files to allow the user to select. The constant is a very convenient way to specify "just show typical file types that QuickTime can handle." You can also pass in null to show all files.

Note

Chapter 4 has more on the FOUR_CHAR_CODE integers used for these "types."

Adding a Controller

This application isn't particularly user-friendly yet—the user can't start or stop the movie, move through it, or set the volume. Fortunately, it's easy to use a MovieController to get the standard QuickTime controller bar, an on-screen control widget that provides a play/pause button, a volume control, and the movie position control (typically called a "scrubber" in QuickTime parlance).

How do I do that?

Create a new class in the source file BasicQTController.java. The main() is exactly the same as before, while the constructor adds one new line and changes another, as seen in Example 2-2.

Example 2-2. Getting a movie component with a controller

public class BasicQTController extends Frame {
 
  public BasicQTController (Movie m) throws QTException {
      super ("Basic QT Controller");
      MovieController mc = new MovieController(m);
      QTComponent qc = QTFactory.makeQTComponent (mc);
      Component c = qc.asComponent( );
      add (c);
      pack( );
  }

Note

Compile and run this example with ant run-ch02-basicqtcontroller.

The result, when run, looks like the application in Figure 2-3. Notice the presence of the classic QuickTime control bar at the bottom of the window.

Figure 2-3. Movie with on-screen controller

Movie with on-screen controller

What just happened?

This time, instead of asking the QTFactory to make a QTComponent from the Movie, the program creates a MovieController object from the Movie and asks the QTFactory to make a QTComponent from that. This eliminates the need for main( ) to call start( ), because the user can start and stop the movie from the play/pause button on the control bar.

Getting a Movie-Playing JComponent

The previous tasks have used the AWT, which seemingly nobody uses anymore. Many developers will want to create a Swing GUI, and thus they need a movie-playing JComponent. QuickTime for Java can provide one.

How do I do that?

Example 2-3 presents a rewrite of the previous BasicQTPlayer that does everything with Swing equivalents (JFrame instead of Frame, JComponent instead of Component, etc.).

Example 2-3. All-Swing simple movie player

package com.oreilly.qtjnotebook.ch02;
 
import quicktime.*;
import quicktime.app.view.*;
import quicktime.std.movies.*;
import quicktime.io.*;
 
import com.oreilly.qtjnotebook.ch01.QTSessionCheck;
 
import java.awt.*;
import javax.swing.*;
 
public class BasicSwingQTPlayer extends JFrame {
 
  public BasicSwingQTPlayer (Movie m) throws QTException {
      super ("Basic Swing QT Player");
      MoviePlayer mp = new MoviePlayer (m);
      QTJComponent qc = QTFactory.makeQTJComponent (mp);
      JComponent jc = qc.asJComponent( );
      getContentPane( ).add (jc);
      pack( );
  }
 
  public static void main (String[  ] args) {
      try {
          QTSessionCheck.check( );
          QTFile file =
              QTFile.standardGetFilePreview (
          QTFile.kStandardQTFileTypes);
          OpenMovieFile omFile = OpenMovieFile.asRead (file);
          Movie m = Movie.fromFile (omFile);
          JFrame f = new BasicSwingQTPlayer (m);
          f.pack( );
          f.setVisible(true);
          m.start( );
      } catch (Exception e) {
          e.printStackTrace( );
      }
  }
}

Note

Compile and run this example with ant run-ch02-basicswingqtplayer.

This produces a simple movie-player window—as seen in Figure 2-4—using Swing, but visually indistinguishable from its AWT equivalent.

Figure 2-4. Playing a movie with a Swing JComponent

Playing a movie with a Swing JComponent

What just happened?

Creating a QTJComponent (read that as "QT JComponent," not "QTJ Component"—I know, it confused everyone on the developer list at first, too) requires an object called a MoviePlayer, which can be simply created from a Movie. This is passed to QTFactory 's makeQTJComponent() method to get a QTJComponent, which in turn can be turned into a Swing JComponent with asJComponent( ) .

What about...

...getting a control bar? Good question. QTJ doesn't provide one for Swing. Remember, the movie's display and the control bar are both native widgets—to display the movie in Swing, the movie has to be drawn to an off-screen region, then painted by Java onto the JComponent so that everything is "lightweight," in Java parlance. QTJ provides this for the movie but not for the control bar (perhaps because it would be difficult for the native QuickTime to keep track of your mouse movements in the Java space), so a developer would need to roll her own Swing widget for controlling the Movie, trapping mouse actions and calling appropriate methods on the Movie or MovieController.

Note

Methods to control a Movie or MovieController are introduced in the next task.

And what about the awful performance? Good catch—depending on your source, the frame rate of this version might be far worse than the AWT equivalent. Think about the earlier paragraph that says the movie needs to be drawn into an off-screen buffer and then reimaged into Swing. Doesn't that sound a little redundant? Think the overhead is going to add up if you need to do it 30 times a second? It is, and it does. Performance of the QTJComponent is awful compared to that of the QTComponent . Not only does QTJ have to do extra work, but it also doesn't score hardware-accelerated graphics benefits it might otherwise be able to achieve by using its native rendering pipeline.

So, I'm going to tell you something that clashes with every other Java GUI book you've ever read: go ahead and mix Swing and AWT widgets . That's right. It's not going to cause blindness, the end of the world, or a drop in your home's resale value.

To be specific, you can freely mix AWT widgets, like the QTComponent, and Swing widgets in the same container as long as they don't overlap. Unless you're doing something tricky with Swing's "glass pane," or possibly the JLayeredPane, you're probably safe.

The common overlap problem comes from menus, both those that descend from the menu bar and pop-up menus. A lightweight Swing menu will go behind any AWT component, and the result isn't pretty. The way around this is to call setLightweightPopupEnabled(false) on all your menus that might overlap with your QTComponent.

By the way, this problem isn't limited to QTJ. Most Java toolkits that use native drawing spaces for performance reasons run into the same issue. Sun's JMF defaults to heavyweight components, as does the OpenGL-to-Java library JOGL. Getting AWT and Swing to play nice is a common issue for Java multimedia developers.

Controlling a Movie Programmatically

For various reasons, an application might want to control the movie via its own method calls, in lieu of or in addition to the GUI provided by QuickTime's MovieController. One example of this is Movie.start( ). You can programmatically issue many more commands, some of which you can't issue with the default control.

How do I do that?

Example 2-4 shows a new class, BasicQTButtons.java. The main() is exactly the same as BasicQTPlayer, but the constructor has extra work to create some control buttons, and an actionPerformed( ) method implements AWT's ActionListener.

Note

Compile and run this example with ant run-ch02-basicqtbuttons..

Example 2-4. Programmatic control of a movie

package com.oreilly.qtjnotebook.ch02;
 
import quicktime.*;
import quicktime.app.view.*;
import quicktime.std.movies.*;
import quicktime.io.*;
 
import com.oreilly.qtjnotebook.ch01.QTSessionCheck;
 
import java.awt.*;
import java.awt.event.*;
 
public class BasicQTButtons extends Frame
  implements ActionListener {
 
  Button revButton,
      stopButton,
      startButton,
      fwdButton;
 
  Movie theMovie;
 
  public BasicQTButtons (Movie m) throws QTException {
      super ("Basic QT Player");
      theMovie = m;
      QTComponent qc = QTFactory.makeQTComponent (m);
      Component c = qc.asComponent( );
      setLayout (new BorderLayout( ));
      add (c, BorderLayout.CENTER);
      Panel buttons = new Panel( );
      revButton = new Button("<");
      revButton.addActionListener (this);
      stopButton = new Button ("0");
      stopButton.addActionListener (this);
      startButton = new Button ("1");
      startButton.addActionListener (this);
      fwdButton = new Button (">");
      fwdButton.addActionListener (this);
      buttons.add (revButton);
      buttons.add (stopButton);
      buttons.add (startButton);
      buttons.add (fwdButton);
      add (buttons, BorderLayout.SOUTH);
      pack( );
  }
 
  public void actionPerformed (ActionEvent e) {
      try {
          if (e.getSource( ) =  = revButton)
              theMovie.setRate (theMovie.getRate( ) - 0.5f);
          else if (e.getSource( ) =  = stopButton)
              theMovie.stop( );
          else if (e.getSource( ) =  = startButton)
              theMovie.start( );
          else if (e.getSource( ) =  = fwdButton)
              theMovie.setRate (theMovie.getRate( ) + 0.5f);
      } catch (QTException qte) {
          qte.printStackTrace( );
      }
  }
 
  public static void main (String[  ] args) {
      try {
          QTSessionCheck.check( );
          QTFile file =
              QTFile.standardGetFilePreview (
                QTFile.kStandardQTFileTypes);
          OpenMovieFile omFile = OpenMovieFile.asRead (file);
          Movie m = Movie.fromFile (omFile);
          Frame f = new BasicQTButtons (m);
          f.pack( );
          f.setVisible(true);
          m.start( );
      } catch (Exception e) {
          e.printStackTrace( );
      }
  }
 
 
}

Run this program to see a display like that shown in Figure 2-5. The buttons call functions to set the rate of the movie. The rate is 0 for a stopped movie, a negative number for a movie playing backward, and a positive number for a movie playing forward. A rate of 1.0 represents normal playing speed, so 0.5 would be half speed, and 2.0 would be double speed. The buttons have the following functions:

<
Reduces the rate by 0.5. For a playing movie (rate = 1.0), clicking this once will cut it to half speed (0.5), twice will stop it (0.0), three times will go to half-speed reverse (-0.5), four times to normal-speed reverse (-1.0), etc.
0
Stops the movie, by way of a call to Movie.stop(), which is the same as Movie.setRate(0).
1
Plays the movie forward at normal speed, equivalent to Movie.setRate(1).
>
Increases the rate by 0.5.

Figure 2-5. Controlling movie play rate with AWT buttons

Controlling movie play rate with AWT buttons

What just happened?

This is a very simple example of methods that can be called to affect a movie's playback. These are the methods a developer creating his own control widget (i.e., ignoring the warning in the previous section) would need to use.

Another useful method is setVolume( ), a self-explanatory method that takes values from 0.0 (silence) to 1.0 (maximum). Also useful is a setTime( ) method, which changes the current position in the movie.

Note

The next task covers QuickTime's concept of time, which is used as the parameter for setTime( ).

What about...

...using some similar methods in MovieController ? A MovieController object, even if it's not used to get an on-screen control widget, provides some methods with equivalent functionality, but with different names and conventions. For example, stop(), start( ), and setRate( ) are all effectively wrapped by a single method, play( ) , which takes a rate argument. MovieController also has some unique functionality, such as only playing the selection, setting "looping" behavior (immediately returning to the beginning when the end is reached, or vice versa), and a method called setPlayEveryFrame() , which will force the movie to not drop frames, even if that requires it to play more slowly than it should.

Showing a Movie's Current Time

Advanced users, particularly those doing editing, would like to know a movie's current time—i.e., where they are in the movie. The scrubber can provide a general idea of the movie's current time, but certain applications call for an exact value.

How do I do that?

Example 2-5s BasicQTTimeDisplay code extends the BasicQTController by adding a Label to the bottom of the Frame . A Swing Timer calls actionPerformed( ) every 250 milliseconds, and this method checks the current time of the movie and resets the label.

Note

The Swing version of Timer is used to ensure that changing the label occurs on the AWT event-dispatch thread.

Compile and run this examnple with ant run-ch02-basicqttimedisplay.

Example 2-5. Displaying the current time of a movie

package com.oreilly.qtjnotebook.ch02;
 
import quicktime.*;
import quicktime.app.view.*;
import quicktime.std.movies.*;
import quicktime.io.*;
 
import com.oreilly.qtjnotebook.ch01.QTSessionCheck;
 
import java.awt.*;
import java.awt.event.*;
 
public class BasicQTTimeDisplay extends Frame
  implements ActionListener {
  Movie theMovie;
  Label timeLabel;
 
  public BasicQTTimeDisplay (Movie m) throws QTException {
      super ("Basic QT Controller");
      theMovie = m;
      MovieController mc = new MovieController(m);
      QTComponent qc = QTFactory.makeQTComponent (mc);
      Component c = qc.asComponent( );
      setLayout (new BorderLayout( ));
      add (c, BorderLayout.CENTER);
      timeLabel = new Label ("-:--", Label.CENTER);
      add (timeLabel, BorderLayout.SOUTH);
      javax.swing.Timer timer =
          new javax.swing.Timer (250, this);
      timer.start( );
      pack( );
  }
 
  public void actionPerformed (ActionEvent e) {
      if (theMovie =  = null)
                  return;
      try {
          int totalSeconds = theMovie.getTime( ) /
                             theMovie.getTimeScale( );
          int seconds = totalSeconds % 60;
          int minutes = totalSeconds / 60;
          String secString = (seconds > 9) ?
              Integer.toString (seconds) :
              ("0" + Integer.toString (seconds));
          String minString = Integer.toString (minutes);
          timeLabel.setText (minString + ":" + secString);
      } catch (QTException qte) {
          qte.printStackTrace( );
      }
  }
 
  public static void main (String[  ] args) {
      try {
          QTSessionCheck.check( );
          QTFile file =
              QTFile.standardGetFilePreview (
                 QTFile.kStandardQTFileTypes);
          OpenMovieFile omFile = OpenMovieFile.asRead (file);
          Movie m = Movie.fromFile (omFile);
          Frame f = new BasicQTTimeDisplay (m);
          f.pack( );
          f.setVisible(true);
          m.start( );
      } catch (Exception e) {
          e.printStackTrace( );
      }
  }
}

This produces the application seen in Figure 2-6.

Figure 2-6. Displaying the current time of a movie

Displaying the current time of a movie

What just happened?

Obviously, some funky math is happening in the actionPerformed( ) method, which uses the Movie 's getTime() and getTimeScale( ) methods to figure out the current time in seconds, from which the program calculates the minutes and seconds portions of label.

QuickTime has a concept of a "time scale," which represents the time-keeping system of a Movie. For a given time scale, n, one unit of time in that time scale is 1/n seconds. So, if a Movie had a time scale of 1,000, the units would be milliseconds. Movies actually default to a time scale of 600, but the actual value is irrelevant—you just have to be sure to work with whatever value the movie uses. getTime( ) returns the movie's current time in terms of the time scale, so for a time scale of 600, if getTime( ) returns 3,600, the current time is exactly 6 seconds into the movie. Other prominent methods that work with the time scale are setTime() and getDuration( ).

Note

When we work with editing commands, we'll see that the Movie selection is also represented with time-scale values like these.

What about...

...just using milliseconds or nanoseconds or something normal instead of this crazy time-scale stuff? Actually, this flexible system of time scales is one of the best things about QuickTime. There needs to be some system of keeping track of time in a Movie, and it's generally desirable for the units to be of a sufficient resolution so that all important times divide evenly—i.e., they can be represented as ints.

Most Java programmers are used to thinking about time in terms of milliseconds, but that's totally inadequate for media. For example, CD audio has 44,100 samples a second, meaning that each sample takes 0.02267 . . . ms. So, that's obviously not going to work. Insisting on some smaller unit (microseconds, nanoseconds, picoseconds, etc.) won't help, because you can never know that it will be good enough for some arbitrary piece of time-based data. QuickTime's system of time scales allows the system of measurement to be ideally suited to the media itself.

An interesting thought about the preceding example is that Movie's default time base of 600 is also inadequate for CD audio. As it turns out, the tracks of a movie (more accurately, the "media" they refer to) can have their own time scales. So, a Movie can have one time scale, its video can have another, and the audio can have a third.

So, why is the default time scale 600? It appears to have originated with the 60 "ticks" per second used for time keeping on the oldest Macs, but it turns out to be a wonderfully common multiple of:

  • 24 (frames per second in film)
  • 25 (frames per second in PAL and SECAM video, used in Europe, Africa, South America, and parts of Asia)
  • 30 (frames per second in NTSC video, used in North America and Japan)

Actually, that last example is not entirely true. NTSC color video is broadcast at an overall rate of 29.97 frames/sec, so to keep things straight, two frame numbers are dropped every minute (except for every 10th minute) to compensate for a synchronization problem in the color signal. QuickTime can handle these "drop-frame" video tracks by making the time scale 2,997 and each frame 100 units long. I told you it was handy!

Listening for Movie State-Changes

One problem with polling to show the current time in the movie is that it's wasteful and inaccurate: it's optimal to check the time only when the movie's playing, and to eliminate latency, it would be nice to be notified when there's a sudden change in the current time, such as when the user slides the scrubber. Fortunately, there's a callback API to notify a program when things like this occur.

How do I do that?

This example revises the BasicQTButtons program. The new version, BasicQTCallback, asks to be notified when the rate changes. When the rate is 0, it will disable the stop button (labeled "0"), and when the rate is 1, it disables the play button (labeled "1"). For space, I'll list only the lines that have changed from BasicQTButtons.

First, there are two new imports: quicktime.std.clocks, which is where callbacks are defined, and quicktime.std, whose StdQTConstants provides constants to specify the callbacks' behavior:

import quicktime.std.*;
import quicktime.std.clocks.*;

Next, the constructor is changed to pass the Movie to an inner class' constructor:

MyQTCallback myCallback = new MyQTCallback (m);

And here's the inner class. It has a constructor that takes a Movie argument and an execute( ) method:

class MyQTCallback extends RateCallBack {
  public MyQTCallback (Movie m) throws QTException {
      super (m.getTimeBase( ),
             0,
             StdQTConstants.triggerRateChange);
      callMeWhen( );
  }
  public void execute( ) {
      if (rateWhenCalled =  = 0.0) {
          startButton.setEnabled (true);
          stopButton.setEnabled (false);
      } else if (rateWhenCalled =  = 1.0) {
          startButton.setEnabled (false);
          stopButton.setEnabled (true);
      }
      // indicate that we want to be called again
      try {
          callMeWhen( );
      } catch (QTException qte) {
          qte.printStackTrace( );
      }
  }
}

The result looks like the window in Figure 2-7. Notice how in the screenshot, the stop button ("0") is dimmed, indicating that the movie is already stopped. If the user hits "1," the movie will play and the play button will be disabled.

Figure 2-7. Disabling buttons via callbacks

Disabling buttons via callbacks

What just happened?

The inner class creates a QTCallBack , specifically a subclass of RateCallBack . In its constructor, it indicates the conditions under which it wants to be called—by passing the triggerRateChange flag, it asks to be called any time the rate changes. It then invokes callMeWhen( ) to actually register the callback.

QuickTime invokes the callback via the execute() method. This implementation checks the rateWhenCalled value, inherited from RateCallBack, to determine if the movie is stopped or started, and enables or disables buttons appropriately. Finally, it issues a new callMeWhen( ) call to ask to get called back on future rate changes—QuickTime callbacks are one-time-only deals, not like the EventListeners that are typical in Java, so programmers have to remember to reregister for new callbacks after every execute().

What about...

...that 0 argument to the RateCallBack's constructor? Good question. This is one of those times where all the interesting values are defined only in the native documentation, not the Javadocs. The third argument, used to trigger the callback on any rate change, can be used with the constants triggerRateEquals, triggerRateNotEqual, triggerRateLT ("less than"), triggerRateLTE ("less than or equals"), etc., to define a behavior when the callback is made only when a certain condition is true. When using these triggers, the middle argument specifies the rate to be compared. For example, a callback could be set up to run only when the movie is playing, by passing 0 and triggerRateNotEquals as the second and third arguments, respectively.

Note

In the previous lab, a "rate not equal to 0" callback could be used to start or stop the time-label polling thread, so it would run only when the movie has a non-zero rate.

Are there other kinds of callbacks? Glad you asked. There are four major callbacks, each with its own class in quicktime.std.clocks:

RateCallBack
Calls back when the rate changes, as seen in the earlier example.
ExtremesCallBack
Calls back when playback reaches the beginning or end of the Movie. Behavior is specified with triggerAtStart or triggerAtStop.
TimeCallBack
Calls back when playback reaches a specific time in the movie. The behavior flag determines if the callback occurs when moving forward (triggerTimeFwd), backward (triggerTimeBwd), or either forward or backward (triggerTimeEither).
TimeJumpCallBack
Calls back when the movie's current time changes in a way that is not consistent with its current play rate. The typical case here is that the user is grabbing the scrubber to move around the movie. Setting up this callback takes no parameters or behavior flags.

And what about more sophisticated callback setup and teardown? This example doesn't need to clean up anything, but a more sophisticated application, one that opens and closes multiple movies, would need to release callback resources. This is done with a call to the QTCallBack 's cancelAndCleanup() method.

There is also a simple cancel() method that can be used to cancel a callback previously registered with callMeWhen() . To change a callback, you must cancel( ) it, then construct a new callback and register it with callMeWhen( ).

Moving Frame by Frame

One popular feature for playback is the ability to step forward exactly one frame. It turns out to be trickier than one might initially expect: it's not like there's a Movie.nextFrame( ) method. Indeed, a Movie might not have a video track at all, if it represents an MP3 or some other audio-only media. So, finding the next frame requires being a little smarter about looking inside the Movie's structure.

How do I do that?

This example builds on the earlier BasicQTButtons code. In this example, the implementations of the forward and back buttons are altered so that instead of changing the play rate, they change the current time to be the next frame before or after the current time. For space, this example shows only the changes from the original BasicQTButtons.

This example needs to import quicktime.std to use StdQTConstants , and quicktime.std.clocks for some time-related classes. It also adds an instance variable visualTrack, which is found with the following call:

theMovie = m;
// find video track
visualTrack =
  m.getIndTrackType (1,
                     StdQTConstants.visualMediaCharacteristic,
                     StdQTConstants.movieTrackCharacteristic);

If a visual track isn't found, the revButton and fwdButton are disabled later in the constructor.

Finally, a new implementation of actionPerformed() does the frame-step logic when the revButton or fwdButton is clicked:

if (e.getSource( ) =  = revButton) {
  TimeInfo ti =
      visualTrack.getNextInterestingTime (
            StdQTConstants.nextTimeMediaSample,
            theMovie.getTime( ),
            -1);
  theMovie.setTime (new TimeRecord (theMovie.getTimeScale( ),
                                    ti.time));
}
else if (e.getSource( ) =  = stopButton)
  theMovie.stop( );
else if (e.getSource( ) =  = startButton)
  theMovie.start( );
else if (e.getSource( ) =  = fwdButton) {
  TimeInfo ti =
      visualTrack.getNextInterestingTime (
            StdQTConstants.nextTimeMediaSample,
            theMovie.getTime( ),
            1);
  theMovie.setTime (new TimeRecord (theMovie.getTimeScale( ),
                                    ti.time));
}

Note

Compile and run this example with ant run-ch02-basicqtstepper.

There's no screenshot for this example, because it's difficult to show a frame step in a static medium like a book.

What just happened?

This program finds the video track with a call to Movie.getIndTrackType() , which takes three arguments:

Which instance to find
This is 1-based, so passing 1 means "find the first matching track."
A search criterion
This is a constant from StdQTConstants that can be a media "type" (videoMediaType, soundMediaType, etc.), or it can be a "characteristic" (videoMediaCharacteristic, audioMediaCharacteristic). The characteristics are helpful in cases like this when several kinds of media are acceptable matches ("visual" media includes video, MPEG, Flash, and more).
Flags to control the search
This should be the value movieTrackMediaType if the previous argument is a media type or movieTrackCharacteristic if it is a characteristic.

An alternative way to find a suitable track would be to iterate over the tracks with Movie.getIndTrack( ), get the Media object from each discovered track, and use instanceof to see if it matches a given media class (VideoMedia, SoundMedia, etc.).

Assuming you can find such a track, the trick to finding the next frame is to use the media's getNextInterestingTime() method. There are several kinds of "interesting times," and to indicate interest in the next frame, which is more accurately the next "sample," you pass the behavior flag nextTimeMediaSample. The method also takes a parameter representing the time in the movie where it should start searching for the next frame (in this case, it's the current time) and the desired search direction (any positive int for a forward search, and any negative int for a backward search).

The value returned by getNextInterestingTime( ) is a TimeInfo object. This program is interested only in the time field of this object, which is represented in the Movie's time scale (not the Media's, interestingly enough). It takes that value and advances the movie to the interesting time—i.e., the next frame—with a call to Movie.setTime( ) .

What about...

...other kinds of times? The native GetMediaNextInterestingTime function offers the following behavior flags:

NextTimeMediaSample
The behavior used in this example.
NextTimeMediaEdit
Finds the next group of samples—i.e., the next thing that has been edited into the movie (editing is covered in the next chapter).
NextTimeSyncSample
Finds the next "sync sample"—i.e., the next sample that is completely self-contained. Many video compression formats send a sync sample (also known as a "key frame"), which is a complete image, while subsequent samples are just information about what has changed since the sync sample. In other words, these later frames aren't complete and cannot be rendered without information from one or more other samples.
NextTimeEdgeOK
Can be OR'ed in with other flags to indicate that it's OK to return the beginning or the end of the media as a valid "interesting time."

What's up with the first track being 1 instead of 0? As a curious legacy, one that feels more like Pascal than Java, most QuickTime methods that do an index-based get are one-based, not zero-based. In fact, if you try to getTrack(0), you'll get a QTException.

Warning

The other gotcha is that although this example is written to work with any visual media, it won't work for MPEG-1 or MPEG-2 files. These files multiplex (or "mux") the audio and video into one stream, and QuickTime doesn't de-mux them in memory, so it has no easy way to find the next video sample. This is why there are separate MPEGMedia and MPEGMediaHandler classes in QTJ; the latter is a subclass of VisualMediaHandler, but it is also implementing AudioMediaHandler . Fortunately, MPEG-4, whose internal structure is friendlier to QuickTime, appears as separate audio and video tracks just like other QuickTime movies.

Playing Movies from URLs

Along with loading movies from disk, QuickTime can also load them from URLs, and is pretty smart about network latency.

How do I do that?

Example 2-6 shows a totally new class, BasicQTURLController.java. This is a significant rethinking of the earlier BasicQTController class. This example creates a GUI from an empty "dummy" movie, then asks the user for a URL, gets a Movie from that, and replaces the dummy movie. By getting the movie last, it tempts fate to see how well QTJ can deal with loading a movie over the network.

Example 2-6. Loading and playing a movie from a URL

package com.oreilly.qtjnotebook.ch02;
 
import quicktime.*;
import quicktime.std.*;
import quicktime.app.view.*;
import quicktime.std.movies.*;
import quicktime.std.movies.media.*;
import quicktime.io.*;
 
import com.oreilly.qtjnotebook.ch01.QTSessionCheck;
 
import java.awt.*;
 
public class BasicQTURLController extends Frame {
 
  QTComponent qc;
 
  public BasicQTURLController ( ) throws QTException {
      super ("Basic QT DataRef/Controller");
      Movie dummyMovie = new Movie( );
      qc = QTFactory.makeQTComponent (dummyMovie);
      Component c = qc.asComponent( );
      add (c);
      pack( );
  }
 
  public static void main (String[  ] args) {
      try {
          QTSessionCheck.check( );
          BasicQTURLController f =
              new BasicQTURLController ( );
          String url =
              javax.swing.JOptionPane.showInputDialog (f,
                                              "Enter URL");
          DataRef dr = new DataRef (url);
          Movie m = Movie.fromDataRef (dr,
                                 StdQTConstants.newMovieActive);
          MovieController mc = new MovieController (m);
          f.qc.setMovieController (mc);
          f.setVisible(true);
          f.pack( );
          m.prePreroll(0, 1.0f);
          m.preroll(0, 1.0f);
          m.start( );
      } catch (Exception e) {
          e.printStackTrace( );
      }
  }
}

Note

Compile and run this example with ant run-ch02-basicqturlcontroller.

When this app is first run, the user sees a dialog asking for a URL. Enter a valid URL (notice that again, for simplicity, the examples don't meaningfully check input or handle errors gracefully). Assuming the URL has valid QuickTime content, the user will see a window like the one shown in Figure 2-8.

Figure 2-8. Movie played from a URL DataRef

Movie played from a URL DataRef

What just happened?

Some different techniques are in play in this example, the most important of which is showing that the Movie or MovieController displayed by a QTComponent can be replaced. The constructor creates a QTComponent from the empty dummyMovie, but after creating a Movie from the URL, a MovieController is created for it and is used to replace the contents of the visible QTComponent via the setMovieController( ) call.

Two helper calls, prePreroll( ) and preroll( ), allocate movie-playing resources up front, to reduce jitter and dropped frames when the movie starts playing. These methods take the same two arguments: the movie time and the rate that the program intends to start playing at.

This example uses a MovieController to make a point. As seen in Figure 2-8, the scrubber has an inner bar that is only partially filled in. This is a graphic representation of how much of the movie data has been downloaded. This example goes ahead and plays the movie immediately, trusting that it will download data faster than we can consume it. This isn't a safe assumption at all—dial-up users will stop almost immediately, though the controller gives them the ability to see how much they have and to play when they're ready.

As for getting the Movie, it's a pretty simple process: pass the URL to a DataRef constructor. These DataRef objects are something of a general-purpose media locator in QuickTime, used here for network access. The new Movie is then created with the fromDataRef( ) call.

Notice the second argument to fromDataRef( ). This is an example of using QuickTime behavior flags , which are found throughout QuickTime. One of the more interesting concepts about the flags is that these behaviors can be combined. The flags are ints with a single bit turned on (meaning their actual values are powers of 2). The idea is that you can mathematically OR them together to combine multiple behaviors. The constants of the java.awt.Font class, like BOLD and ITALIC, work pretty much the same way. In this case, in addition to making the movie active, the program could set a behavior flag to tell QuickTime not to enable alternate tracks (if there are any), by making a call like this:

Movie m = Movie.fromDataRef (dr,
             StdQTConstants.newMovieActive |
             StdQTConstants.newMovieDontAutoAlternate);

The other flags mentioned for this call, newMovieDontResolveDataRefs and newMovieDontAskUnresolvedDataRefs, deal with esoteric cases where a movie is not self-contained and some of the media it refers to can't be found.

Warning

The Javadocs for Movie.fromDataRef( ) advocate using the behavior flag StdQTConstants4.newMovieAsyncOK . That was useful in the old QTJ, but when used in this example in QTJ 6.1, it might allow the QTComponent to decide that your movie has zero height and zero width, because the movie gets handed to the QTComponent before the size metadata gets downloaded. As Figure 2-8 shows, the preceding code does not block and wait for the whole movie to be downloaded. Advice for now: don't use it unless you think you're blocking on Movie.fromDataRef( ).

Preventing "Tasking" Problems

All the tasks in this chapter have managed to avoid one of the more obscure hazards in QuickTime. This example tempts fate and exposes the problem by playing a movie that would otherwise freeze up.

How do I do that?

Example 2-7 reprises the command-line audio player from the first chapter, which takes a path to a file as a command-line argument, builds a Movie, and plays it, without getting any kind of GUI.

Example 2-7. Playing audio from the command line

package com.oreilly.qtjnotebook.ch01;
 
import quicktime.*;
import quicktime.app.time.*;
import quicktime.io.*;
import quicktime.std.*;
import quicktime.std.movies.*;
 
import java.io.*;
 
public class BasicAudioPlayer {
 
  public static void main (String[  ] args) {
      if (args.length != 1) {
          System.out.println (
              "Usage: BasicAudioPlayer <file>");
          return;
      }
      try {
          QTSessionCheck.check( );
          QTFile f = new QTFile (new File(args[0]));
          OpenMovieFile omf = OpenMovieFile.asRead(f);
          Movie movie = Movie.fromFile (omf);
          TaskAllMovies.addMovieAndStart( );
          movie.start( );
      } catch (QTException e) {
          e.printStackTrace( );
      }
  }
}

Notice the line in bold. Take it out, recompile, and watch what happens. The program will likely hang or immediately exit, playing just a spurt of audio or none at all.

What just happened?

QuickTime movies need to explicitly be given CPU time to do their work: reading from disk or the network, decompressing and decoding, rendering to the screen, or playing to the speakers. This process is called "tasking." Looking at the Javadocs reveals that the Movie class has a task( ) method that could be called to give time to a specific movie, and a static taskAll( ) method that tasks all active movies.

Managing all these calls manually and being sure to call them frequently enough would be, of course, incredibly tedious. That's why QTJ provides TaskAllMovies , a wrapper for a Thread whose job is to call task() on all active movies. This example kicks off TaskAllMovies (assuming nothing else has done so), thereafter allowing it to be blissfully unaware of tasking.

What about...

...all the other examples? Why are we only hearing about this now? Well, TaskAllMovies is so useful that QTJ itself uses it extensively. Any time a program works with QTJ's GUI classes, by getting a Component for a Movie or MovieController, it picks up calls to TaskAllMovies automatically. In fact, it's a little difficult not to pick up automatic tasking calls from QTJ, short of opening audio-only movies with non-QTJ GUI widgets, or no GUI at all, as seen here.

Note

It's still important to know about tasking in case you stumble intosuch a case and can't figure out why your application is just sitting there.

Tip

In the last section, a warning mentioned an edge case where using the newMovieAsyncOK flag might give you a QTComponent with zero size because Movie.fromDataRef( ) returned immediately, before enough of the movie could be loaded to know how big it was.

Tasking helps you solve this problem. After fromDataRef(), you would go into a while loop, testing whether Movie.getBox( ) returns non-zero dimensions. If it doesn't, call task( ) on the movie to give QuickTime a chance to load more of it, maybe do a Thread.sleep() or Thread.yield( ) to keep Java happy, and go back to the top of the while. Because QuickTime movies usually, but don't always, have metadata early in the file, an alternative to testing the size of the movie would be to call maxLoadedTimeInMovie() on the Movie object and wait for a non-zero value—this would also be better if there's any chance the Movie is audio only.

But seriously, it's not going to happen because you don't need newMovieAsyncOK. Chill.

In QTJ 6.0 and earlier, there were also URL-loading scenarios where a program might need to task( ) a few times to download enough of the Movie to read in the metadata and get a valid size, but this behavior seems to have changed in 6.1, making explicit tasking even more of an edge case.

Personal tools