/*********************************************************************************** * * THIS SOFTWARE IS COPYRIGHT STEVEN MORRIS 2002. ALL RIGHTS RESERVED * * THIS SOFTWARE IS FREE FOR NON COMMERCIAL USE FOR THE PURPOSE OF LEARNING MHP. ANY * USE FOR OTHER PURPOSES IS PROHIBITED UNLESS WRITTEN AGREEMENT IS OBTAINED. * * DISTRIBUTION OF THIS CODE IS PROHIBITED */ // Import the standard package that we need to be an Xlet import javax.tv.xlet.*; // These are the classes that we need for using the media control API in MHP import org.davic.net.dvb.DvbLocator; import org.davic.resources.*; import javax.media.*; import javax.tv.media.*; import org.dvb.media.*; // These classes are needed when we resize a JMF Player. import java.awt.Dimension; import java.awt.Point; import java.awt.Rectangle; // To make sure everything looks good when we are resizing the video, we need // to set the color of the background plane. Doing this needs the HAVi UI API. import org.havi.ui.*; import java.awt.Color; /** * This class sets up a JMF Player and plays a DVB service. It also * manipulates the Player in various ways. First, the video will * displayed full-screen. The Xlet then scales to he video to * quarter-screen and displays it in the top right corner for * the screen, before moving it to the other corners in sequence. * Finally, the video is scaled backto full-screen. * * If the Xlet is paused during this sequence, it will restart at * the same point in the sequence when it resumes. * * Since this is quite a complex Xlet that may take a while to finish * doing its work, we do most of the work in a separate thread. For * this reason, the Xlet implements the Runnable interface. It also * implements the javax.media.ControllerListener interface to receive * events related to the JMF Player that it's manipulating. */ public class JMFExample implements Xlet, Runnable, ControllerListener, ResourceClient { // A private variable to store our Xlet context. In this case, we do actually // use this, showing why it's a good idea to keep a reference to the Xlet context. private XletContext context; // A private variable that keeps a reference to the thread that // will do all of the work. private Thread myWorkerThread; // A private field to hold the current state. This is needed because the startXlet() // method is called both to start the Xlet for the first time and also to make the // Xlet resume from the paused state. This filed lets us keep track of whether we're // starting for the first time. private boolean hasBeenStarted; /************************************************************************** * * The methods below this line are the standard Xlet methods. These mostly do * nothing surprising,since we do most of the work in a separate thread. */ /** * A default constructor, included for completeness. */ public JMFExample() { } /** * Initialise the Xlet. In this case, we have no special initialisation * that we need to do. */ public void initXlet(javax.tv.xlet.XletContext context) throws javax.tv.xlet.XletStateChangeException { // We keep a reference to our Xlet context because we will need it later. this.context = context; } /** * Start the Xlet. This is where we actually start doing useful work. */ public void startXlet() throws XletStateChangeException { // Check if the Xlet has been started and resuming from the paused state, // or whether it's being started for the first time. If it's being // started for the first time, we reate a new thread to do the work if (!hasBeenStarted) { // startXlet() should not block for too long, and waiting for sections // to be filtered is definitely too long. To solve this, we start // another thread to do the work. myWorkerThread = new Thread(this); myWorkerThread.start(); } else { // If we're resuming from the paused state, we don't need to set // up the thread. doResume(); } } /** * Pause the Xlet. */ public void pauseXlet() { // The doPause() method does everything we need to pause // the Xlet doPause(); } /** * Destroy the Xlet. */ public void destroyXlet(boolean unconditional) throws javax.tv.xlet.XletStateChangeException { //tell the user that the method has been called System.out.println("destroyXlet() called"); if (unconditional) { // We have been ordered to terminate, so we obey the order politely and release any //scarce resources that we are holding. } else { // We have had a polite request to die, so we can refuse this request if we want. throw new XletStateChangeException("Please don't let me die!"); } } /************************************************************************** * * The methods below this point are the ones which actually do all of the work * related to creating and manipulating the Player. */ // This variable will hold a reference to the JMF Player that we will use. Player player; // This will hold a reference to the JMF Control that we will // use for scaling the video; BackgroundVideoPresentationControl scalingControl; // The BackgroundVideoPresentationControl uses // VideoTransformation objects to define where the video // should appear and at what size. This variable keeps a // reference to the transformation that we will use to // modify the video. VideoTransformation transformation; // This will be used to store the default transformation, so // that we can put the video back the way we found it. VideoTransformation defaultTransformation; // Since we will manipulate the vidoe in several ways, this variable is // used to keep track of what manipulation we're currently doing. This // way, if the Xlet is paused, we can resume where we left off int currentState; // One of the problems in MHP is the number of Locator classes // that are used. While they are all related in some way, // keeping track of which one to use can be tough. JMF // uses MediaLocator objects to refer to different media // content. MediaLocator targetVideo; // This is used to store a reference to the background device for our display. HBackgroundDevice backdevice; /** * The main method for the worker thread. This is where most of the work is done. */ public void run(){ if (!hasBeenStarted) { hasBeenStarted = true; // Create a MediaLocator object that refers to the // service we will be playing. targetVideo = new MediaLocator("dvb://142.100.3"); // Set the current state to be 0 - the beginning // of the sequence; currentState = 0; } // We need to set the colour of the background plane when // we're manipulating the size of the vido, or twe may // get ugly results. The MHP specification doesn't say // what the background colour should be when the video // is less than full-screen. We do this every time we // are resumed in case somethng else has changed it. setHaviBackgroundColour(); // Now play the video. playVideo(); } /** * This method sets the background colour of the STB, so that we don't get * ugly results when we scale the video. The background colour may be * undefined, which could be a problem. To solve this, we just set the * background colour to be black. */ public void setHaviBackgroundColour() { // We use the HAVi UI API to set the colour in the background plane. // We will assume that we are using the default HScreen (since an // MHP receiver will normally only have one HScreen) and the // default background device. or more information about using the // HAVi UI API, take a look at the graphics tutorial or the HAVi // example Xlet. HBackgroundConfiguration backconfig; HScreen screen = HScreen.getDefaultHScreen(); backdevice = screen.getDefaultHBackgroundDevice(); // Get the current configuration for the background device. Since // we only want to change the background colour, we can leave // everything else the same. backconfig = backdevice.getCurrentConfiguration(); // Reserve the background device so that we can change the // colour. backdevice.reserveDevice(this); // Now set the background colour. We may not have the permission to do this try { backconfig.setColor(Color.black); } catch (HPermissionDeniedException e) { // Ignore it, since it's not very important } catch (HConfigurationException e) { // We can't do this in the current configuration. We can ignore it // because it's only cosmetic. } } /** * Create the JMF player and play the video. */ private void playVideo() { // The media control API can throw a number of exceptions from // various methods. When we're doing this kind of manipulation, // it can fail in a number of interesting ways, so we enclose // this entire piece of code in a 'try' block try { // Set up the player and get it presenting video. We need // to do this in every case, since we will have destroyed // the player if we've paused the Xlet. // This variable is used to hold the result of the // transformation operations boolean result; // We already have a MediaLocator for the service we want to play, // so we can create a Player that will display that content. player = javax.media.Manager.createPlayer(targetVideo); // This class will be a listener for events from the JMF Player. // This allows us to track state transitions within the Player // to make sure it's in the state we think it's in. // While it's not so important in a small application such as // this it's much more necessary in a bigger app. player.addControllerListener(this); // We will also get a reference to the background video scaling // and position control for our player. We do this by calling // getControl() on the player with the fully-qualified class // name of the control we want. scalingControl = (org.dvb.media.BackgroundVideoPresentationControl) player.getControl("org.dvb.media.BackgroundVideoPresentationControl"); // We're now ready to start the player. In this case, we won't // call every methods that takes us through the various states // in the Player, because we don't have to. We can simply call // start() on the Player and wait for it to start up. The other // methods are more useful if you want finer control over the // timing of when the player actually starts. player.start(); // This lets us resume the sequence in the same place that we // paused, and shows how we could do this in a complex Xlet // that follows a sequence of actions with no user interaction. // Each of the case blocks will fall through to the next, so // when the switch statement is entered, it will jump to the // current state (which we will increment at the end of // every case block) and then continue the sequence, falling // through each case block after that until the thread is // killed. switch (currentState) { case 0: { // Scale the video to quarter screen // Error handling in case we don't have a scaling control // (which some emulators such as XleTView do not) if (scalingControl == null) { break; } // First, we create a VideoTransformation object representing the // transformation that we want to carry out. In this case, we scale // from the size of the source video to quarter sized video in the // top right corner of the screen. // We could set the parameters in the constructor, but we'll do // it separately to make it easier to understand. transformation = new VideoTransformation(); // We scale the video to half-size in each dimension transformation.setScalingFactors((float)0.5, (float)0.5); // We change the position so that the video is in the top right // corner of the screen. The position is set using HAVi // normalised co-ordinates and represents the top left corner // of the video. transformation.setVideoPosition(new HScreenPoint((float)0.5, (float)0.0)); // Now we're ready to scale the video. We do this by setting // the video presentation control to use the transformation // that we have just created. First, though, we have to check // that this transformation can actually be mad. Calling // getClosestMatch() will return the transformation that does // the best job of matching the one we have specified. transformation = scalingControl.getClosestMatch(transformation); result = scalingControl.setVideoTransformation(transformation); // Increment the current state, so that it we get paused we // resume at the next block currentState++; // As before, we sleep for a while so that the change can take effect // and so that we have a chance to see it. Thread.sleep(5000); // ********FALL THROUGH******** } case 1: { // Move the video to the top left corner // Error handling in case we don't have a scaling control // (which some emulators such as XleTView do not) if (scalingControl == null) { break; } // We change the transformation so that the position // of the view is in ther top left corner transformation.setVideoPosition(new HScreenPoint((float)0.0, (float)0.0)); // As before, we have to get the supported transformation // that is the closest match to this. Depending on the // capabilities of our scaling hardware, this may be the // same as the previous transformation transformation = scalingControl.getClosestMatch(transformation); // Now apply the transformation result = scalingControl.setVideoTransformation(transformation); // Increment the current state, so that it we get paused we // resume at the next block currentState++; // Again, we sleep for a while so that we can see the change. Thread.sleep(10000); // ********FALL THROUGH******** } case 2: { // Do the same thing, moving the video to the bottom right // Error handling in case we don't have a scaling control // (which some emulators such as XleTView do not) if (scalingControl == null) { break; } transformation.setVideoPosition(new HScreenPoint((float)0.5, (float)0.5)); transformation = scalingControl.getClosestMatch(transformation); result = scalingControl.setVideoTransformation(transformation); // Increment the current state, so that it we get paused we // resume at the next block currentState++; // Again, we sleep for a while so that we can see the change. Thread.sleep(10000); // ********FALL THROUGH******** } case 3: { // Finally, move the video to the bottom left // Error handling in case we don't have a scaling control // (which some emulators such as XleTView do not) if (scalingControl == null) { break; } transformation.setVideoPosition(new HScreenPoint((float)0.0, (float)0.5)); transformation = scalingControl.getClosestMatch(transformation); result = scalingControl.setVideoTransformation(transformation); // Increment the current state, so that it we get paused we // resume at the next block currentState++; // Again, we sleep for a while so that we can see the change. Thread.sleep(10000); // ********FALL THROUGH******** } case 4: { // Scale the video back to full screen // Error handling in case we don't have a scaling control // (which some emulators such as XleTView do not) if (scalingControl == null) { break; } // We do exactly the same, but scale the video back to full-screen. // In this case, we use the default transformation, which was the // one used by the player to begin with. This is typically a // full-screen player. We don't nee dot get the closest match, // since we know this transformaton is supported. result= scalingControl.setVideoTransformation(defaultTransformation); // Increment the current state, so that it we get paused we // resume at the next block currentState++; } } // Lastly, we sleep for a while longer so that the user can see the // effects of the last transformation. Thread.sleep(10000); // We're now done with the sequence, so we stop the player // and free the resources. // Stop the Player player.stop(); // Calling deallocate() will free any resources that the Player uses // and leave it in a state where that Player instance can't be used // again. We don't really need to call stop() before we do this, // because calling deallocate() on a running Player will automatically // stop it, but it's good practise to do it explicitly. player.deallocate(); } catch (Exception e) { // How we deal with the exception will depend on what exception is // actually thrown and how our application is using the media. In // this case, we'll take the tried and tested approach of printing // a stack trace and dying. System.out.println("Exception thrown while using the JMF Player:"); e.printStackTrace(); } } /** * Take the actions needed to pause the Xlet. In this case, that means * stopping the section filters and releasing them, since they are * scarce resources. We also kill the filtering thread, since this * isn't needed once the Xlet is paused and we should kill unnecessary * threads when we pause ourselves. */ private void doPause() { // Release the resources that we're holding (in the case, the Player) // Stop the Player player.stop(); // Calling deallocate() will free any resources that the Player uses // and leave it in a state where that Player instance can't be used // again. We don't really need to call stop() before we do this, // because calling deallocate() on a running Player will automatically // stop it, but it's good practise to do it explicitly. player.deallocate(); // Now kill the thread myWorkerThread.stop(); } /** * Take the actions needed to resume the Xlet from the paused state. * In our case, that means creating a new player (because we * destroyed the old one) and continuing to play the video */ private void doResume() { // Restart the worker thread. This will do the necessary // setup and resume where it was paused. myWorkerThread.start(); } /** * This method is inherited from javax.media.ControllerListener. It is * called whenever the state of a JMF Player changes, enabling us to * track the state of the player during the application. */ public void controllerUpdate(ControllerEvent event) { // This currently just prints the event. In a larger application, // we should probably make sure that the parts of the application // that rely on a Player being in a certain state are aware of // any state changes. System.out.println("We got a JMF event - " + event); } /************************************************************************** * * These methods are inherited from the ResourceClient interface and are used * to tell the application when it has lost access to its resources (or * when it is about to lose access to them). This gives the application a * chance to clean up when it loses access to a resource, and gives it a * chance to handle things gracefully. */ /** * This method gets called when the resource manager requests that we give * up a resource. We can refuse to do so, and that's what we do in this * case (even though we shouldn't). */ public boolean requestRelease(ResourceProxy proxy, Object requestData) { return false; } /** * This method gets called when the resource manager informs us that we must * release a resource */ public void release(ResourceProxy proxy) { // release control of the background device if (backdevice != null) backdevice.releaseDevice(); } /** * This method gets called when the resource manager tells us we've * lost access to a resource and we should clean up after ourselves. */ public void notifyRelease(ResourceProxy proxy) { // release control of the background device. Even though we don't // have control of it, this makes sure everything is synchronized. if (backdevice != null) backdevice.releaseDevice(); } }