/***********************************************************************************
 *
 * 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();
	}


}