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

advertisement

AddThis Social Bookmark Button

Playing Movies in a Java 3D World, Part 2:
Pages: 1, 2, 3

3. A Frame-By-Frame Movie

To understand the inner workings of QTSnapper, it helps to have a general idea of the structure of a QuickTime movie. Each movie may be composed of multiple audio and video tracks, overlapping in time. The general idea is illustrated by Figure 4.

Figure 4
Figure 4. The internals of a QuickTime movie

Each track manages its own data, such as the type of media it contains, and the media itself. The media container has its own data structures, including its duration and playing rate (i.e., how many samples should be shown per second). The media is a series of samples (or frames), the first one starting at time 0 (with respect to media time). The samples are indexed, with the first sample at position 1 (not 0).

Simplified views of QuickTime's track and media structures are given in Figure 5.

Figure 5
Figure 5. The internals of a QuickTime track and media

For more comprehensive information, have a look at the movie section of the QuickTime tutorial.

Accessing the Movie's Video Media

The QTSnapper constructor opens the movie:


// globals
private boolean isSessionOpen = false;
private OpenMovieFile movieFile;
private Movie movie;


// in the constructor,
// start a QuickTime session
QTSession.open();
isSessionOpen = true;

// open the movie
movieFile = 
   OpenMovieFile.asRead( new QTFile(fnm) );
movie = Movie.fromFile(movieFile);

The call to QTSession.open() initializes QuickTime prior to its use. There should be a corresponding call to QTSession.close() at termination time.

The video track is located (if one exists) and its media accessed:


// more globals
private Track videoTrack;
private Media vidMedia;


// in the constructor, 
// extract the video track from the movie
videoTrack = 
    movie.getIndTrackType(1, 
            StdQTConstants.videoMediaType,
            StdQTConstants.movieTrackMediaType);
if (videoTrack == null) {
  System.out.println("Sorry, not a video");
  System.exit(0);
}

// get the media used by the video track
vidMedia = videoTrack.getMedia();

Once the media is exposed, various information is extracted from it:


// more globals
private MediaSample mediaSample;
private int numSamples;  // number of samples
private int sampIdx;     // current sample index
private int width;       // frame width
private int height;      // frame height


// in the constructor
numSamples = vidMedia.getSampleCount();

sampIdx = 1;   // get first sample in the track
mediaSample = vidMedia.getSample(0, 
  vidMedia.sampleNumToMediaTime(sampIdx).time,1);

// store width and height of image in the sample
ImageDescription imgDesc = 
      ImageDescription) mediaSample.description;
width = imgDesc.getWidth();
height = imgDesc.getHeight();

sampIdx is a counter that will iterate through the samples (the first sample starts at position 1).

The movie image's width and height are obtained by examining the first sample, under the assumption that all of the samples use the same dimensions.

Measuring FPS

The number of frames per second returned by QTSnapper will be used later to compare different implementation strategies for the class. The necessary elements are initialized in the constructor:


// frame rate globals
private long startTime;
private long numFramesMade;

// initialize them in the constructor
startTime = System.currentTimeMillis(); 
numFramesMade = 0;

Wrapping Up

As the application is about to terminate, stopMovie() is called in QTSnapper. It reports the FPS, and shuts down QuickTime.


// globals
private DecimalFormat frameDf = 
    new DecimalFormat("0.#"); // 1 dp


synchronized public void stopMovie()
{
  if (isSessionOpen) {
    // report frame rate
    long duration = 
        System.currentTimeMillis() - startTime;
    double frameRate = 
       ((double) numFramesMade*1000.0)/duration;
    System.out.println("FPS: " + 
                  frameDf.format(frameRate));

    QTSession.close();  // close down QuickTime
    isSessionOpen = false;
  }
}

stopMovie() and getFrame() are synchronized so that it's impossible to terminate the QuickTime session while a frame is being copied from the movie.

Catching a Frame

getFrame() returns a single sample (a frame) from the movie as a BufferedImage object. The frame is selected using the index number stored in sampIdx (which goes from 1 to numSamples, and then repeats).


// globals
private BufferedImage img, formatImg;


synchronized public BufferedImage getFrame()
{
  if (!isSessionOpen)
    return null;
  if (sampIdx > numSamples)   
    // start back with the first sample
    sampIdx = 1;

  try {
    /* Get the sample starting at the 
       specified index time */
    TimeInfo ti = 
       vidMedia.sampleNumToMediaTime(sampIdx);
    mediaSample=vidMedia.getSample(0,ti.time,1);
    sampIdx++;

    writeToBufferedImage(mediaSample, img);

    // resize img, writing it to formatImg
    Graphics g = formatImg.getGraphics();
    g.drawImage(img, 0, 0, 
         FORMAT_SIZE, FORMAT_SIZE, null); 

    // Overlay current time on image
    g.setColor(Color.RED);
    g.setFont(
      new Font("Helvetica", Font.BOLD, 12));
    g.drawString(timeNow(), 5, 14);
    g.dispose();

    numFramesMade++;  // count frame
  }
  catch (Exception e) {
    System.out.println(e);
    formatImg = null;
  }

  return formatImg;
} // end of getFrame()

The sample is readily obtained by calling the getSample() method from QTJ's Media class. Unfortunately, there's still the tricky question of converting the sample into a BufferedImage, which I've hidden away inside of my writeToBufferedImage() method.

The method performs some fancy translation dance steps, which I lifted from Chris W. Johnson's MovieFrameExtractor.java example, available from the quicktime-java mailing list.

The gory details, copiously commented, can be studied in the code. A "raw" image is extracted from the sample, and then decompressed as it's written into a QuickTime version of a Graphics object. The uncompressed data in the Graphics object is copied into another "raw" image, and then into a pixel array (a PixMap). Finally, this array is written into the DataBuffer part of an empty BufferedImage.

Does the Application Work? Does it Work Well?

Yes, Movie3D displays a movie, but large movies play slowly. This is due to getFrame()'s slowness in supplying frames, which can be quantified by looking at the FPS numbers.

For the movie in Figure 1, the reported FPS on a slow Windows 98 machine is in the range of 15 to 17 frames per second. However, the TimeBehavior object is requesting an update every 40 milliseconds, which should translate into frames appearing at nearer to 25 FPS.

getFrame() is slow because of the time-consuming conversion of the sample to a BufferedImage. As the current call to getFrame() gets bogged down converting a frame, further requests are delayed until the current one is finished.

I'll look at two ways of attacking this problem: permitting getFrame() to skip frames when it finally gets to process a request, and trying a different conversion strategy in getFrame(). I'll look at each of these in turn, starting with frame skipping.

Pages: 1, 2, 3

Next Pagearrow