Playing Media

One of the most common uses of the JMF is for playing media: audio or video. As already illustrated in the previous chapter with BBPApplet, playing media with the JMF is a relatively simple process because of the abstraction from format details provided by the JMF.

The JMF provides the Player class for playing media. Player creation is achieved through the Manager class, as described in a previous section, by specifying the location of the media. A Player object provides methods and additional associated objects (Control objects) for controlling the playback: starting and stopping, setting the rate of play, and so on. Indeed the Player class exposes graphical (AWT) Components for control and visualization of the playback. These can be added directly to a GUI, preventing the need for the user to construct a large number of GUI components and linking those to actions (methods) on the Player object.

Player Interface

A Player is a MediaHandler object for controlling and rendering media. It is the chief class within the JMF for combined handling and rendering and provides an abstracted, high-level model of the process to user code that separates it from lower-level considerations such as the format of the media tracks. Hence, code employing a Player sees a single object with the same methods and attributes, regardless of the particulars of the media being played.

The Player interface directly extends that of Controller. Thus, a Player not only provides all the methods detailed for a Controller, but also supports the five-state model of stopped time (Unrealized, Realizing, Realized, Prefetching, Prefetched) of Controller. Understanding the Player interface requires an understanding of the Controller interface. However only the extensions of Player to the Controller interface will be discussed here; readers needing to reacquaint themselves with the features of Controller should revisit the first section of this chapter.

Creation of a Player (with the exception of the MediaPlayer Bean, which is discussed in the next subsection) is achieved through the central Manager class. As discussed in the earlier section on the Manager, there are six methods for creating a Player; three for creating an Unrealized Player and three for creating a Realized Player. A Player can be created on the basis of a URL, MediaLocator, or DataSource. The following code fragment is typical of Player creation:

    String mediaName = "ftp://ftp.cs.adfa.edu.au/media/someExample.wav";
    MediaLocator locator = new MediaLocator(mediaName);
    Player thePlayer = Manager.createPlayer(locator);

Keeping track of the status and maintaining control of a Player is achieved through the same mechanism as that for Controller discussed earlier. Objects can implement the ControllerListener interface and be added as listeners to the Player. The Player posts events (discussed in the Controller section of the chapter) indicating state transitions (for example, Realizing to Realized), errors, and other relevant media events (for example, end of media reached, duration of media now known).

A number of the methods of Player, most inherited from Controller, exert direct control over the state of a Player. Others can alter the state as a side effect. The following list shows those methods:

realize()— Moves the Player from Unrealized to Realized. Asynchronous (returns immediately).

prefetch()— Moves the Player from Realized to Prefetched. Asynchronous (returns immediately).

start()— Moves the Player from whatever state it is in currently to Started. The Player will transition through any intermediary states and send notice of such transitions to any listeners. Asynchronous (returns immediately).

stop()— Moves the Player from Started state back to either (depending on the type of media being played) Prefetched or Realized. Synchronous (returns only after Player is actually stopped).

deallocate()— Deallocates the resources acquired by the Player and moves it to the Realized state (if at least Realized at the time of call) or to Unrealized (if in Realizing or Unrealized at time of call). Synchronous.

close()— Closes down the Player totally. It will post a ControllerClosedEvent. The behavior of any further calls on the Player is unspecified: The Player shouldn't be used after it is closed. Synchronous.

setStopTime()— Sets the media time at which it will stop. When the media reaches that time, the Player will act as though stop() was called. Asynchronous (returns immediately and stops play when time reaches that specified).

setMediaTime()— Changes the media's current time. This can result in a state transition. For instance, a started Player might return to Prefetching while it refills buffers for the new location in the stream. Asynchronous (in that any state changes triggered aren't waited for).

setRate()— Sets the rate of playback. Can result in a state change on a started Player for similar reasons to those for setMediaTime(). Asynchronous (in that any state changes triggered aren't waited for).

As you can see, many (of the most important) methods are asynchronous, returning to the caller immediately but before any state transitions can occur. This is particular true of starting a Player: Gathering the resources necessary to play media can be a time-consuming process.

This in turn illustrates the need to listen to a Player: User code must listen to the events generated by a Player so that it can determine, for example, when to place control components on the screen or when to show (and remove) download progress bars.

The Player class extends Controller with the addition of six methods as shown in Figure 8.18. One method is extremely useful and commonly employed. The start() method will transition a Player object through all intermediary stopped states, regardless of starting state (for example, Unrealized), to Started. This is the most common way to start a Player because it lifts the burden of calculating an appropriate synchronization time to be passed to the syncStart() methods. State transitions are still reported to all ControllerListener's listening to the Player. Thus it is still possible to initiate appropriate actions (for example, obtain control and visual Components) as the Player passes through the appropriate states.

Figure 8.18. The Player interface.


Two other methods of Player, getControlPanelComponent() and getVisualComponent(), find high usage. These are used to obtain AWT Components suitable for controlling and displaying the output (playback) of the Player, respectively. These methods are only valid on a Player that is in a Realized, or higher (closer to Started) state. Calling them on an Unrealized player will result in a NotRealizedError exception being thrown. Hence, it is necessary to listen to the Player's events to ascertain when these Components can safely be obtained. Further, the methods return null if there isn't a valid Component, such as is the case with a visual Component for purely audio media. The user should always check that the returned object isn't null before it is used (generally added to a GUI).

Finally, the GainControl object obtained with getGainControl() can be used to control the gain (volume) of playback. The addController() and removeController() methods are provided for cases in which a single Player controls the synchronized handling (generally other Players) of multiple media. This is discussed briefly in a subsequent subsection.

The MediaPlayer Bean

The JMF directly exposes one class that implements the Player interface: MediaPlayer. MediaPlayer is a fully featured JMF Player encapsulated as a Java Bean.

Playing media with the MediaPlayer Bean is quite simple and painless because not only are various methods provided for configuring the behavior of the Bean, but it also handles the transition from playing one media to another when a different location is set. Set the PlayerOfMedia example application in the following section for user code to deal with that issue.

The following code fragment shows how simple setting up and starting a MediaPlayer can be. In this case, the fragment sets the MediaPlayer to play the media in videos/example1.mov once.

Player mp = new MediaPlayer();
mp.setLoop(false);
mp.setMediaLocation("file://videos.example1.mov");
mp.start();

Controlling Multiple Players/Controllers

It is possible through the addController() and removeController() methods of Player to employ a single Player object to manage a number of Controllers. This is an approach that allows playback (or indeed processing) among a number of Players to be synchronized by being driven by a single Player.

There are a number of details and caveats to the approach, and users considering such synchronization should carefully read the Player class specification, which includes a lengthy discussion of this topic. However, the fundamental model of the approach is that Controllers added to a Player fall under the control of that Player. Method calls on the Player are also made on the added Controllers, and events from the Controllers can propagate back to those listening to the Player object.

Application: Player of Media

This subsection discusses an example application, PlayerOfMedia, that illustrates a number of the features of the Controller, Player, and other classes already discussed.

As its name indicates, PlayerOfMedia is an application capable of rendering time-based media. Through a simple menu system, the user might select to open (for play) any media. He might then enter the URL of the media or browse the local filesystem to select a file. If the application is unable to create a Player for the media, the user is informed via a pop-up dialog box. Otherwise, the application adds the appropriate visual and control components to the GUI, resizing itself appropriately, and hands control to the user (through the control component). When the end of the media is reached, play restarts from the beginning.

In terms of core functionality, the ability to play media, PlayerOfMedia, isn't dissimilar to the BBPApplet example from Chapter 7. However, PlayerOfMedia provides further features in its ability for users to specify the actual media they want played. That requires the handling of not only arbitrary media, but also more importantly graceful handling of errors or exceptions as well as resource management. If a new Player object is created, the old Player object (if one exists) and its associated resources must be freed up.

Listing 8.7 shows the source of the application, which can also be found on the book's companion Web site. It is worth noting that the greater portion of the code doesn't concern the JMF per se, but rather the graphical user interface (simple as it is) that the program provides. The creation of menus and dialog boxes is one of the main reasons for the size of the code. Figure 8.19 shows the application as the user is playing one media (a video) and about to replace it with another (a WAVE file of didgeridoo play).

Figure 8.19. The PlayerOfMedia GUI application running and displaying a video while the user is about to open a different media file.


The two methods, createAPlayer() and controllerUpdate(), directly concern the JMF. The createAPlayer() method is responsible for creating a new Player and moving it to the Realized state. It is passed a String, which is the name and location of the media to be played. However, before a new Player can be created, the method ascertains if there is an existing Player object. If so, it is stopped and any resources it had acquired are freed with the close() call. Similarly, the old Player is no longer listened to (events generated from it won't be received). A MediaLocator is constructed and passed to the Manager class in order to create the new Player. Any resulting exceptions are caught and the user informed. Otherwise, if the process of creation was successful, the application starts listening to the new Player as well as directing it to become Realized.

The controllerUpdate() method is required for any class that implements the ControllerListener interface and is where nearly all the control of the Player object occurs. The method is called with events generated by the currently listened to Player object. These typically represent state transition events, such as from Realizing to Realized, but can also include errors or information about the duration of the media. The method reacts to the particular type of event received and acts accordingly, which perhaps alters its user interface, moves the Player along to the next appropriate state, or even posts an error message. In particular, a RealizeComplete event means that visual and control Components can be obtained for the new Player object. Any old Components (from the previous Player) or download progress bars should first be discarded; then the new ones should be obtained and added to the GUI. Reaching the end of play (signaled by an EndOfMedia event) is dealt with by setting the media's own time to zero (back to the start) and restarting the Player.

Listing 8.7 The PlayerOfMedia GUI application that Allows the User to Select the Media He Wants to Play
import java.awt.*;
import java.awt.event.*;
import javax.media.*;
import javax.media.protocol.*;
import javax.media.control.*;
import java.io.*;
/*****************************************************************
* A Graphical application allowing the user to choose the media
* they wish to play.
* PlayerOfMedia presents a Dialog in which the user may enter
* the URL of the media to play, or select a file from the local
* system.
*
* @author Spike Barlow
******************************************************************/
public class PlayerOfMedia extends Frame implements ActionListener,
      ControllerListener {

  /** Location of the media. */
  MediaLocator  locator;

  /** Player for the media */
  Player        player;

  /** Dialog for user to select media to play. */
  Dialog        selectionDialog;

  /** Buttons on user dialog box. */
  Button        cancel,
                open,
                choose;

  /** Field for user to enter media filename */
  TextField     mediaName;

  /** The menus */
  MenuBar       bar;
  Menu          fileMenu;
  /** Dialog for informing user of errors. */
  Dialog        errorDialog;
  Label         errorLabel;
  Button        ok;

  /** Graphical component for controlling player. */
  Component     controlComponent;

  /** Graphical component showing what isbeing played. */
  Component     visualComponent;

  /** Graphical component to show download progress. */
  Component     progressBar;

  /** Sizes to ensure Frame is correctly sized. */
  Dimension     controlSize;
  Dimension     visualSize;
  int           menuHeight = 50;

  /** Directory user last played a file from. */
  String        lastDirectory = null;

  /** Flags indicating conditions for resizing the Frame. */
  protected static final int  VISUAL = 1;
  protected static final int  PROGRESS = 2;


/*************************************************************
* Construct a PlayerOfMedia. The Frame will have the default
* title of "Player of Media". All initial actions on the
* PlayerOfMedia object are initiated through its menu
* (or shotcut key).
**************************************************************/
PlayerOfMedia() { this("Player of Media"); }

/*************************************************************
* Construct a PlayerOfMedia. The Frame will have the title
* supplied by the user. All initial actions on the
* PlayerOfMedia object are initiated through its menu
* (or shotcut key).
**************************************************************/
PlayerOfMedia(String name) {

  super(name);
  ///////////////////////////////////////////////////////////
  // Setup the menu system: a "File" menu with Open and Quit.
  ///////////////////////////////////////////////////////////
  bar = new MenuBar();
  fileMenu = new Menu("File");
  MenuItem openMI = new MenuItem("Open...",
new MenuShortcut(KeyEvent.VK_O));
  openMI.setActionCommand("OPEN");
  openMI.addActionListener(this);
  fileMenu.add(openMI);
  MenuItem quitMI = new MenuItem("Quit",
new MenuShortcut(KeyEvent.VK_Q));
  quitMI.addActionListener(this);
  quitMI.setActionCommand("QUIT");
  fileMenu.add(quitMI);
  bar.add(fileMenu);
  setMenuBar(bar);

  ///////////////////////////////////////////////////////
  // Layout the frame, its position on screen, and ensure
  // window closes are dealt with properly, including
  // relinquishing the resources of any Player.
  ///////////////////////////////////////////////////////
  setLayout(new BorderLayout());
  setLocation(100,100);
  addWindowListener(new WindowAdapter() {
    public void windowClosing(WindowEvent e) {
      if (player!=null) { player.stop(); player.close();}
      System.exit(0); } });

  /////////////////////////////////////////////////////
  // Build the Dialog box by which the user can select
  // the media to play.
  /////////////////////////////////////////////////////
  selectionDialog = new Dialog(this,"Media Selection");
  Panel pan = new Panel();
  pan.setLayout(new GridBagLayout());
  GridBagConstraints gbc = new GridBagConstraints();
  mediaName =new TextField(40);
  gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth=2;
  pan.add(mediaName,gbc);
  choose = new Button("Choose File...");
  gbc.ipadx = 10; gbc.ipady = 10;
  gbc.gridx = 2; gbc.gridwidth= 1; pan.add(choose,gbc);
  choose.addActionListener(this);
  open = new Button("Open");
  gbc.gridy = 1; gbc.gridx = 1; pan.add(open,gbc);
  open.addActionListener(this);
  cancel = new Button("Cancel");
  gbc.gridx = 2; pan.add(cancel,gbc);
  cancel.addActionListener(this);
  selectionDialog.add(pan);
  selectionDialog.pack();
  selectionDialog.setLocation(200,200);

  ////////////////////////////////////////////////////
  // Build the error Dialog box by which the user can
  // be informed of any errors or problems.
  ////////////////////////////////////////////////////
  errorDialog = new Dialog(this,"Error",true);
  errorLabel = new Label("");
  errorDialog.add(errorLabel,"North");
  ok = new Button("OK");
  ok.addActionListener(this);
  errorDialog.add(ok,"South");
  errorDialog.pack();
  errorDialog.setLocation(150,300);

  Manager.setHint(Manager.PLUGIN_PLAYER,new Boolean(true));
}

/**********************************************************
* React to menu selections (quit or open) or one of the
* the buttons on the dialog boxes.
***********************************************************/
public void actionPerformed(ActionEvent e) {

  if (e.getSource() instanceof MenuItem) {
    //////////////////////////////////////////////////
    // Quit and free up any player acquired resources.
    //////////////////////////////////////////////////
    if (e.getActionCommand().equalsIgnoreCase("QUIT")) {
      if (player!=null) {
        player.stop();
        player.close();
      }
      System.exit(0);
    }
    /////////////////////////////////////////////////////////
    // User to open/play media. Show the selection dialog box.
    /////////////////////////////////////////////////////////
    else if (e.getActionCommand().equalsIgnoreCase("OPEN")) {
      selectionDialog.show();
    }
  }
  //////////////////////
  // One of the Buttons.
  //////////////////////
  else {
    /////////////////////////////////////////////////////////////
    // User to browse the local file system. Popup a file dialog.
    /////////////////////////////////////////////////////////////
    if (e.getSource()==choose) {
      FileDialog choice = new FileDialog(this,
"Media File Choice",FileDialog.LOAD);
      if (lastDirectory!=null)
        choice.setDirectory(lastDirectory);
      choice.show();
      String selection = choice.getFile();
      if (selection!=null) {
        lastDirectory = choice.getDirectory();
        mediaName.setText("file://"+ choice.getDirectory() +
selection);
      }
    }
    ///////////////////////////////////////////////
    // User chooses to cancel opening of new media.
    ///////////////////////////////////////////////
    else if (e.getSource()==cancel) {
      selectionDialog.hide();
    }
    ///////////////////////////////////////////////////////
    // User has selected the name of the media. Attempt to
    // create a Player.
    ///////////////////////////////////////////////////////
    else if (e.getSource()==open) {
      selectionDialog.hide();
      createAPlayer(mediaName.getText());
    }
    ////////////////////////////////////////////
    // User has seen error message. Now hide it.
    ////////////////////////////////////////////
    else if (e.getSource()==ok)
      errorDialog.hide();
  }
}


/****************************************************************
* Attempt to create a Player for the media who's name is passed
* the the method. If successful the object will listen to the
* new Player and start it towards Realized.
*****************************************************************/
protected void createAPlayer(String nameOfMedia) {

  ////////////////////////////////////////////////////////////
  // If an existing player then stop listening to it and free
  // up its resources.
  ////////////////////////////////////////////////////////////
  if (player!=null) {
    System.out.println("Stopping and closing previous player");
    player.removeControllerListener(this);
    player.stop();
    player.close();
  }

  ///////////////////////////////////////////////////////////
  // Use Manager class to create Player from a MediaLocator.
  // If exceptions are thrown then inform user and recover
  // (go no further).
  //////////////////////////////////////////////////////////
  locator = new MediaLocator(nameOfMedia);
  try {
    System.out.println("Creating player");
    player = Manager.createPlayer(locator);
  }
  catch (IOException ioe) {
    errorDialog("Can't open " + nameOfMedia);
    return;
  }
  catch (NoPlayerException npe) {
    errorDialog("No player available for " + nameOfMedia);
    return;
  }

  //////////////////////////////////////////////////////////
  // Player created successfully. Start listening to it and
  // realize it.
  //////////////////////////////////////////////////////////
  player.addControllerListener(this);
  System.out.println("Attempting to realize player");
  player.realize();
}


/************************************************************
* Popup a dialog box informing the user of some error. The
* passed argument isthe text of the message.
*************************************************************/
protected void errorDialog(String errorMessage) {

  errorLabel.setText(errorMessage);
  errorDialog.pack();
  errorDialog.show();
}


/**************************************************************
* Resize the Frame (window) due to the addition or removal of
* Components.
***************************************************************/
protected void resize(int mode) {
  //////////////////////////////////////////
  // Player's display and controls in frame.
  //////////////////////////////////////////
  if (mode==VISUAL) {
    int maxWidth = (int)Math.max(controlSize.width,visualSize.width);
    setSize(maxWidth,
controlSize.height+visualSize.height+menuHeight);
  }
  ////////////////////////////////
  // Progress bar (only) in frame.
  ////////////////////////////////
  else if (mode==PROGRESS) {
    Dimension progressSize = progressBar.getPreferredSize();
    setSize(progressSize.width,progressSize.height+menuHeight);
  }
  validate();
}

/****************************************************************
* React to events from the player so as to drive the presentation
* or catch any exceptions.
*****************************************************************/
public synchronized void controllerUpdate(ControllerEvent e) {

  ///////////////////////////////////////
  // Events from a "dead" player. Ignore.
  ///////////////////////////////////////
  if (player==null)
    return;

  ////////////////////////////////////////////////////////////
  // Player has reached realized state. Need to tidy up any
  // download or visual components from previous player. Then
  // obtain visual and control components for the player,add
  // them to the screen and resize window appropriately.
  ////////////////////////////////////////////////////////////
  if (e instanceof RealizeCompleteEvent) {
    ////////////////////////////////////////////////////
    // Remove any inappropriate Components from display.
    ////////////////////////////////////////////////////
    if (progressBar!=null) {
      remove(progressBar);
      progressBar = null;
    }
    if (controlComponent!=null) {
      remove(controlComponent);
      validate();
    }
    if (visualComponent!=null) {
      remove(visualComponent);
      validate();
    }
    ///////////////////////////////////////////////////////
    // Add control and visual components for new player to
    // display.
    //////////////////////////////////////////////////////
    controlComponent = player.getControlPanelComponent();
    if (controlComponent!=null) {
      controlSize = controlComponent.getPreferredSize();
      add(controlComponent,"Center");
    }
    else
      controlSize = new Dimension(0,0);
    visualComponent = player.getVisualComponent();
    if (visualComponent!=null) {
      visualSize = visualComponent.getPreferredSize();
      add(visualComponent,"North");
    }
    else
      visualSize = new Dimension(0,0);
    //////////////////////////////////////////////////////////
    // Resize frame for new components and move to prefetched.
    //////////////////////////////////////////////////////////
    resize(VISUAL);
    System.out.println("Player is now pre-fetching");
    player.prefetch();
  }
  ////////////////////////////////////////////////////////////
  // Provide user with a progress bar for "lengthy" downloads.
  ////////////////////////////////////////////////////////////
  else if (e instanceof CachingControlEvent &&
player.getState() <= Player.Realizing && progressBar==null) {
    CachingControlEvent cce = (CachingControlEvent)e;
    progressBar = cce.getCachingControl().getControlComponent();
    if (progressBar!=null) {
      add(progressBar,"Center");
      resize(PROGRESS);
    }
  }
  ////////////////////////////////////////////////
  // Player initialisation complete. Start it up.
  ////////////////////////////////////////////////
  else if (e instanceof PrefetchCompleteEvent) {
    System.out.println("Pre-fetching complete, now starting");
    player.start();
  }
  ///////////////////////////////////////////////////////
  // Reached end of media. Start over from the beginning.
  ///////////////////////////////////////////////////////
  else if (e instanceof EndOfMediaEvent) {
    player.setMediaTime(new Time(0));
    System.out.println("End of Media – restarting");
    player.start();
  }
  //////////////////////////////////////////////////////////////
  // Some form of error. Free up all resources associated with
  // the player, don't listen to it anymore, and inform the
  // user.
  //////////////////////////////////////////////////////////////
  else if (e instanceof ControllerErrorEvent) {
    player.removeControllerListener(this);
    player.stop();
    player.close();
    errorDialog("Controller Error, abandoning media");
  }

}


/******************************************************************
* Create a PlayerOfMedia object and pop it up on the screen for the
* user to interact with.
*******************************************************************/
public static void main(String[] args) {

  PlayerOfMedia  ourPlayer = new PlayerOfMedia();
  ourPlayer.pack();
  ourPlayer.setSize(200,100);
  ourPlayer.show();
}
}

Playing Media with a Processor

One of the desirable extensions to the PlayerOfMedia application would be for it to provide a number of statistics about the media it is playing: frame rate, size, duration, content type, and codec. However, using a Player, most of that information simply isn't available because a Player provides no control over any of the processing that it performs on the media, nor over how it renders the media itself. The very abstraction that makes it relatively simple to write programs that play media also makes it impossible to determine much information about the media being played.

An alternative to using a Player object to play (render) media is to use a Processor object. Processor objects are discussed in detail in a later section of this chapter. Details aside, a Processor is really just a specialized type of Player that allows control over the processing that is performed on the input media stream. The output of a Processor is either a DataSource or it is rendered (played).

In order to force a Processor to render the media rather than outputting it, Processor's setContentDescriptor() method should be passed null. This will be revisited in the Processor section, but passing the null reference implies do not create an output DataSource (that is, render the media).

Processors provide all the user interface control features of Players as well as the ability to access the individual tracks that compose the DataSource. Through this mechanism, their formats can be ascertained and such a report generated. The subsequent example MediaStatistics shows how such a report can be ascertained. A program to play media that also reported statistics on the media would then employ a Processor (rather than Player) and combine the features of PlayerOfMedia and MediaStatistics.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset