FileDocCategorySizeDatePackage
BasicPlayer.javaAPI DocJ2ME MIDP 2.029622Thu Nov 07 12:02:26 GMT 2002com.sun.mmedia

BasicPlayer.java

/*
 * @(#)BasicPlayer.java	1.69 02/09/11 @(#)
 *
 * Copyright (c) 1996-2002 Sun Microsystems, Inc.  All rights reserved.
 * PROPRIETARY/CONFIDENTIAL
 * Use is subject to license terms.
 */

package com.sun.mmedia;

import java.io.*;
import java.util.Vector;
import javax.microedition.io.*;
import javax.microedition.media.*;
import java.util.Hashtable;
import javax.microedition.media.control.*;

/**
 * BasicPlayer provides basic implementation for the Player methods.
 * Many of the methods call do<method> to do the actual work that can
 * be overridden by subclasses.
 */
public abstract class BasicPlayer implements Player, VolumeControl {

    /**
     * global player id 
     */
    private static int pcount = -1;

    /**
     * hastable to map playerID to instances
     */
    private static Hashtable mplayers = new Hashtable(4);

    /**
     * lock object 
     */
    private static Object idLock = new Object();

    /**
     * the locator of this player
     */
    private String locator;

    /**
     * the state of this player
     */
    int state = UNREALIZED;

    /**
     * the loopCount of this player
     */
    int loopCountSet = 1, loopCount;

    /**
     * the flag to indicate whether the Player is currently paused at EOM.
     * If true, the Player will seek back to the beginning when
     * restarted.
     */
    boolean EOM = false;

    /**
     * the flag to indicate looping after EOM.
     */
    boolean loopAfterEOM = false;

    /**
     * this player's playerlisteners
     */
    Vector listeners = new Vector(2);

    /**
     * Asynchronous event mechanism.
     */
    EvtQ evtQ = null;

    /**
     * event queue lock obj
     */
    Object evtLock = new Object();

    /**
     * player ID of this player
     */
    protected int pID = 0;

    /**
     * the default constructor
     */
    public BasicPlayer() {
	synchronized (idLock) {
	    pcount = (pcount+1) % 32767;
	    pID = pcount;
	}
	mplayers.put(new Integer(pID), this);
    }

    
    /**
     * Check to see if the Player is closed.  If the
     * unrealized boolean flag is true, check also to
     * see if the Player is UNREALIZED.
     *
     * @param unrealized the flag whether to check the unrealized state.
     */
    protected void chkClosed(boolean unrealized) {
	if (state == CLOSED || (unrealized && state == UNREALIZED)) { 
	    throw new IllegalStateException("The Player is " +
			(state == CLOSED ? "closed" : "unrealized"));
	}
    }


    /**
     * Set the number of times the <code>Player</code> will loop
     * and play the content.
     * <p>
     * By default, the loop count is one.  That is, once started,
     * the <code>Player</code> will start playing from the current
     * media time to the end of media once.
     * <p>
     * If the loop count is set to N where N is bigger than one,
     * starting the <code>Player</code> will start playing the
     * content from the current media time to the end of media.
     * It will then loop back to the beginning of the content
     * (media time zero) and play till the end of the media.
     * The number of times it will loop to the beginning and
     * play to the end of media will be N-1.
     * <p>
     * Setting the loop count to 0 is invalid.  An
     * <code>IllegalArgumentException</code> will be thrown.
     * <p>
     * Setting the loop count to -1 will loop and play the content
     * indefinitely.
     * <p>
     * If the <code>Player</code> is stopped before the preset loop
     * count is reached either because <code>stop</code> is called,
     * calling <code>start</code> again will
     * resume the looping playback from where it was stopped until it
     * fully reaches the preset loop count.
     * <p>
     * An <i>END_OF_MEDIA</i> event will be posted
     * every time the <code>Player</code> reaches the end of media.
     * If the <code>Player</code> loops back to the beginning and
     * starts playing again because it has not completed the loop
     * count, a <i>STARTED</i> event will be posted.
     *
     * @param count indicates the number of times the content will be
     * played.  1 is the default.  0 is invalid.  -1 indicates looping
     * indefintely.
     * @exception IllegalArgumentException Thrown if the given
     * count is invalid.
     * @exception IllegalStateException Thrown if the
     * <code>Player</code> is in the <i>STARTED</i>
     * or <i>CLOSED</i> state.
     */
    public void setLoopCount(int count) 
	throws IllegalArgumentException, IllegalStateException {
	chkClosed(false);
	if (state == STARTED)
	    throw new IllegalStateException("setLoopCount");
	if (count == 0 || count < -1)
	    throw new IllegalArgumentException("setLoopCount");
	loopCountSet = count;
	loopCount = count;
    }


    /**
     * Constructs portions of the <code>Player</code> without
     * acquiring the scarce and exclusive resources.
     * This may include examining media data and may
     * take some time to complete.
     * <p>
     * When <code>realize</code> completes successfully,
     * the <code>Player</code> is in the
     * <i>REALIZED</i> state.
     * <p>
     * If <code>realize</code> is called when the <code>Player</code> is in
     * the <i>REALIZED</i>, <i>PREFETCHTED</i> or <i>STARTED</i> state,
     * the request will be ignored.
     *
     * @exception IllegalStateException Thrown if the <code>Player</code>
     * is in the <i>CLOSED</i> state.
     * @exception MediaException Thrown if the <code>Player</code> cannot
     * be realized.
     * @exception SecurityException Thrown if the caller does not
     * have security permission to realize the <code>Player</code>.
     *
     */
    public synchronized void realize() throws MediaException {
	chkClosed(false);

	if (state >= REALIZED)
	    return;

	doRealize();
	state = REALIZED;
    }

    /**
     * The worker method to realize the player.
     *
     */
    abstract protected void doRealize() throws MediaException;


    /**
     * Acquires the scarce and exclusive resources
     * and processes as much data as necessary
     * to reduce the start latency.
     * <p>
     * When <code>prefetch</code> completes successfully,
     * the <code>Player</code> is in
     * the <i>PREFETCHED</i> state.
     * <p>
     * If <code>prefetch</code> is called when the <code>Player</code>
     * is in the <i>UNREALIZED</i> state,
     * it will implicitly call <code>realize</code>.
     * <p>
     * If <code>prefetch</code> is called when the <code>Player</code>
     * is already in the <i>PREFETCHED</i> state, the <code>Player</code>
     * may still process data necessary to reduce the start
     * latency.  This is to guarantee that start latency can
     * be maintained at a minimum.
     * <p>
     * If <code>prefetch</code> is called when the <code>Player</code>
     * is in the <i>STARTED</i> state,
     * the request will be ignored.
     * <p>
     * If the <code>Player</code> cannot obtain all
     * of the resources it needs, it throws a <code>MediaException</code>.
     * When that happens, the <code>Player</code> will not be able to
     * start.  However, <code>prefetch</code> may be called again when
     * the needed resource is later released perhaps by another
     * <code>Player</code> or application.
     *
     * @exception IllegalStateException Thrown if the <code>Player</code>
     * is in the <i>CLOSED</i> state.
     * @exception MediaException Thrown if the <code>Player</code> cannot
     * be prefetched.
     * @exception SecurityException Thrown if the caller does not
     * have security permission to prefetch the <code>Player</code>.
     *
     */
    public synchronized void prefetch() throws MediaException {
	chkClosed(false);

	if (state >= PREFETCHED)
	    return;

	if (state < REALIZED)
	    realize();
		
	doPrefetch();

	state = PREFETCHED;
    }

    /**
     * the worker method to prefetch the player
     */
    abstract protected void doPrefetch() throws MediaException;


    /**
     * Starts the <code>Player</code> as soon as possible.
     * If the <code>Player</code> was previously stopped
     * by calling <code>stop</code>,
     * it will resume playback
     * from where it was previously stopped.  If the
     * <code>Player</code> has reached the end of media,
     * calling <code>start</code> will automatically
     * start the playback from the start of the media.
     * <p>
     * When <code>start</code> returns successfully,
     * the <code>Player</code> must have been started and
     * a <code>STARTED</code> event will
     * be delivered to the registered <code>PlayerListener</code>s.
     * However, the <code>Player</code> is not guaranteed to be in
     * the <i>STARTED</i> state.  The <code>Player</code> may have
     * already stopped (in the <i>PREFETCHED</i> state) because
     * the media has 0 or a very short duration.
     * <p>
     * If <code>start</code> is called when the <code>Player</code>
     * is in the <i>UNREALIZED</i> or <i>REALIZED</i> state,
     * it will implicitly call <code>prefetch</code>.
     * <p>
     * If <code>start</code> is called when the <code>Player</code>
     * is in the <i>STARTED</i> state,
     * the request will be ignored.
     *
     * @exception IllegalStateException Thrown if the <code>Player</code>
     * is in the <i>CLOSED</i> state.
     * @exception MediaException Thrown if the <code>Player</code> cannot
     * be started.
     * @exception SecurityException Thrown if the caller does not
     * have security permission to start the <code>Player</code>.
     */
    public synchronized void start() throws MediaException {
	chkClosed(false);

	if (state >= STARTED)
	    return;

	if (state < REALIZED)
	    realize();

	if (state < PREFETCHED)
	    prefetch();

	// If it's at the EOM, it will automatically
	// loop back to the beginning.
	if (EOM)
	    setMediaTime(0);

	if (!doStart())
	    throw new MediaException("start");

	state = STARTED;
	sendEvent(PlayerListener.STARTED, new Long(getMediaTime()));
    }

    /** 
     * The worker method to actually start the player
     *
     * @return Whether the player is successfully started
     */
    abstract protected boolean doStart();


    /**
     * Stops the <code>Player</code>.  It will pause the playback at
     * the current media time.
     * <p>
     * When <code>stop</code> returns, the <code>Player</code> is in the
     * <i>PREFETCHED</i> state.
     * A <code>STOPPED</code> event will be delivered to the registered
     * <code>PlayerListener</code>s.
     * <p>
     * If <code>stop</code> is called on
     * a stopped <code>Player</code>, the request is ignored.
     *
     * @exception IllegalStateException Thrown if the <code>Player</code>
     * is in the <i>CLOSED</i> state.
     */
    public synchronized void stop() {
	chkClosed(false);

	loopAfterEOM = false;
	
	if (state < STARTED)
	    return;

	doStop();

	state = PREFETCHED;
	sendEvent(PlayerListener.STOPPED, new Long(getMediaTime()));
    }

    /**
     * the worker method to stop the player
     */
    abstract protected void doStop();


    /**
     * Release the scarce or exclusive
     * resources like the audio device acquired by the <code>Player</code>.
     * <p>
     * When <code>deallocate</code> returns, the <code>Player</code>
     * is in the <i>UNREALIZED</i> or <i>REALIZED</i> state.
     * <p>
     * If the <code>Player</code> is blocked at
     * the <code>realize</code> call while realizing, calling
     * <code>deallocate</code> unblocks the <code>realize</code> call and
     * returns the <code>Player</code> to the <i>UNREALIZED</i> state.
     * Otherwise, calling <code>deallocate</code> returns the
     * <code>Player</code> to  the <i>REALIZED</i> state.
     * <p>
     * If <code>deallocate</code> is called when the <code>Player</code>
     * is in the <i>UNREALIZED</i> or <i>REALIZED</i>
     * state, the request is ignored.
     * <p>
     * If the <code>Player</code> is <code>STARTED</code>
     * when <code>deallocate</code> is called, <code>deallocate</code>
     * will implicitly call <code>stop</code> on the <code>Player</code>.
     *
     * @exception IllegalStateException Thrown if the <code>Player</code>
     * is in the <i>CLOSED</i> state.
     */
    public synchronized void deallocate() {
	chkClosed(false);

	loopAfterEOM = false;
	
	if (state < PREFETCHED)
	    return;

	if (state == STARTED)
	    stop();

	doDeallocate();
	state = REALIZED;
    }

    /**
     * the worker method to deallocate the player
     */
    abstract protected void doDeallocate();


    /**
     * Close the <code>Player</code> and release its resources.
     * <p>
     * When the method returns, the <code>Player</code> is in the
     * <i>CLOSED</i> state and can no longer be used.
     * A <code>CLOSED</code> event will be delivered to the registered
     * <code>PlayerListener</code>s.
     * <p>
     * If <code>close</code> is called on a closed <code>Player</code>
     * the request is ignored.
     */
    public synchronized void close() {
	if (state == CLOSED)
	    return;
	
	deallocate();
	doClose();

	state = CLOSED;
	try {
	    if (stream != null)
		stream.close();
	} catch (IOException e) { }
	sendEvent(PlayerListener.CLOSED, null);
	mplayers.remove(new Integer(pID));
    }

    /**
     * the worker method to close the player
     */
    abstract protected void doClose();
    

    /**
     * Sets the <code>Player</code>'s <i>media time</i>.
     * <p>
     * For some media types, setting the media time may not be very
     * accurate.  The returned value will indicate the
     * actual media time set.
     * <p>
     * Setting the media time to negative values will effectively
     * set the media time to zero.  Setting the media time to
     * beyond the duration of the media will set the time to
     * the end of media.
     * <p>
     * There are some media types that cannot support the setting
     * of media time.  Calling <code>setMediaTime</code> will throw
     * a <code>MediaException</code> in those cases.
     *
     * @param now The new media time in microseconds.
     * @return The actual media time set in microseconds.
     * @exception IllegalStateException Thrown if the <code>Player</code>
     * is in the <i>UNREALIZED</i> or <i>CLOSED</i> state.
     * @exception MediaException Thrown if the media time
     * cannot be set.
     * @see #getMediaTime
     */
    public synchronized long setMediaTime(long now) throws MediaException {
	chkClosed(true);

	if (now < 0)
	    now = 0;
	
	long theDur = getDuration();
	if ((theDur != TIME_UNKNOWN) && (now > theDur))
	    now = theDur;

	long rtn = doSetMediaTime(now);
	EOM = false;

	return rtn;
    }

    /**
     * The worker method to actually set player's media time.
     *
     * @param now The new media time in microseconds.
     * @return The actual media time set in microseconds.
     * @exception MediaException Thrown if an error occurs
     * while setting the media time.
     */
    abstract protected long doSetMediaTime(long now) throws MediaException;


    /**
     * Gets this <code>Player</code>'s current <i>media time</i>.
     * If the <i>media time</i> cannot be determined,
     * <code>getMediaTime</code> returns <code>TIME_UNKNOWN</code>.
     *
     * @return The current <i>media time</i> in microseconds or
     * <code>TIME_UNKNOWN</code>.
     * @exception IllegalStateException Thrown if the <code>Player</code>
     * is in the <i>CLOSED</i> state.
     * @see #setMediaTime
     */
    public long getMediaTime() {
	chkClosed(false);
	return doGetMediaTime();
    }
    

    /**
     * The actual worker method to gets this player's 
     * current <i>media time</i> in microseconds.
     * 
     * @return The current <i>media time</i> in microseconds.
     */
    abstract protected long doGetMediaTime();

    /**
     * Gets the current state of this <code>Player</code>.
     * The possible states are: <i>UNREALIZED</i>,
     * <i>REALIZED</i>, <i>PREFETCHED</i>, <i>STARTED</i>, <i>CLOSED</i>.
     *
     * @return The <code>Player</code>'s current state.
     */
    public int getState() {
	/**
	 * A race condition can occur between
	 * the return of this method and the execution of
	 * a state changing method.
	 */
	return state;
    }


    /**
     * Get the duration of the media.
     * The value returned is the media's duration
     * when played at the default rate.
     * <br>
     * If the duration cannot be determined (for example, the
     * <code>Player</code> is presenting live
     * media)  <CODE>getDuration</CODE> returns <CODE>TIME_UNKNOWN</CODE>.
     *
     * @return The duration in microseconds or <code>TIME_UNKNOWN</code>.
     * @exception IllegalStateException Thrown if the <code>Player</code>
     * is in the <i>CLOSED</i> state.
     */
    public long getDuration() {
	chkClosed(false);
	return doGetDuration();
    }

    /**
     * The actual worker method to retrieve the duration.
     *
     * @return A <CODE>long</CODE> object representing the duration or 
     * TIME_UNKNWON.
     */
    abstract protected long doGetDuration();


    /**
     * Add a player listener for this player.
     *
     * @param playerListener the listener to add.
     * If <code>null</code> is used, the request will be ignored.
     * @exception IllegalStateException Thrown if the <code>Player</code>
     * is in the <i>CLOSED</i> state.
     * @see #removePlayerListener
     */
    public void addPlayerListener(PlayerListener playerListener) {
	chkClosed(false);
	if (playerListener != null)
	    listeners.addElement(playerListener);
    }


    /**
     * Remove a player listener for this player.
     *
     * @param playerListener the listener to remove.
     * If <code>null</code> is used or the given
     * <code>playerListener</code> is not a listener for this
     * <code>Player</code>, the request will be ignored.
     * @exception IllegalStateException Thrown if the <code>Player</code>
     * is in the <i>CLOSED</i> state.
     * @see #addPlayerListener
     */
    public void removePlayerListener(PlayerListener playerListener) {
	chkClosed(false);
	listeners.removeElement(playerListener);
    }


    /**
     * Deliver the events to the player listeners.
     *
     * @param evt the evt type
     * @param evtData the data associated with this event.
     *
     */
    public void sendEvent(String evt, Object evtData) {

	//  There's always one listener for EOM -- itself.
	if (listeners.size() == 0 && evt != PlayerListener.END_OF_MEDIA)
	    return;

	// Deliver the event to the listeners.
	synchronized (evtLock) {
	    if (evtQ == null)
		evtQ = new EvtQ(this);
	    evtQ.sendEvent(evt, evtData);
	}
    }

    /**
     * the worker method to deliver EOM event
     */
    synchronized void doLoop() {

	// If a loop count is set, we'll loop back to the beginning.
	if ((loopCount > 1) || (loopCount == -1)) {
	    try {
		if (setMediaTime(0) == 0) {
		    if (loopCount > 1)
			loopCount--;
		    start();
		} else
		    loopCount = 1;
	    } catch (MediaException ex) {
		loopCount = 1;
	    }
	} else if (loopCountSet > 1)
	    loopCount = loopCountSet;

	loopAfterEOM = false;
    }

    /**
     * Obtain the collection of <code>Control</code>s
     * from this player.
     * @return the collection of <code>Control</code> objects.
     */
    public Control[] getControls() {
	chkClosed(true);
	return new Control[] { this };
    }
    
    /**
     * Gets the <code>Control</code> that supports the specified 
     * class or interface. The full class
     * or interface name should be specified.
     * <code>Null</code> is returned if the <code>Control</code>
     * is not supported.
     *
     * @param type the class name of the <code>Control</code>.  
     * @return <code>Control</code> for the class or interface
     * name.
     */
    public Control getControl(String type) {
	chkClosed(true);

	// Prepend the package name if the type given does not
	// have the package prefix.
	if (type.indexOf('.') < 0)
	    return doGetControl("javax.microedition.media.control." + type);
	
	return doGetControl(type);
    }


    /**
     * The worker method to actually obtain the control.
     *
     * @param type the class name of the <code>Control</code>.  
     * @return <code>Control</code> for the class or interface
     * name.
     */

    abstract protected Control doGetControl(String type);


    /**
     * ===============================
     * For global PlayerID management
     * =============================== 
    */
    /**
     * Obtain a BasicPlayer instance based on the global id.
     *
     * @param pid the given global id.
     * @return the instance of BasicPlayer associated with given global id
     *
     */
    public static BasicPlayer get(int pid) {
	return (BasicPlayer)(mplayers.get(new Integer(pid)));
    }


    /**
     * ==========================
     * Methods for VolumeControl.
     * ==========================
     */
    /**
     * volume level
     */
    private int  level = -1;
    /**
     * mute state
     */
    private boolean mute;

    /**
     * The worker method to actually obtain the control.
     *
     * @param vol the volume level to be set.  
     * @return the actual level has been set.
     */

    protected abstract int doSetLevel(int vol);

    /**
     * set player mute.
     *
     * @param mute the flag to mute the player or not
     *
     */
    public void setMute(boolean mute) {
	if (mute && !this.mute) {
	    doSetLevel(0);
	    this.mute = true;
	    sendEvent(PlayerListener.VOLUME_CHANGED, this);
	} else if (!mute && this.mute) {
	    this.level = doSetLevel(level);
	    this.mute = false;
	    sendEvent(PlayerListener.VOLUME_CHANGED, this);
	}
    }

    /**
     * Check if this player is muted.
     *
     * @return The mute state.
     */
    public boolean isMuted() {
	return mute;
    }
    
    /**
     * Set the volume using a linear point scale 0 to 100.
     * @param ll The new volume specified in the level scale.
     * @return The level that was actually set.
     */
    public int setLevel(int ll) {
	int newl;

	if (ll < 0) {
	    ll  = 0;
	} else if (ll > 100) {
	    ll = 100;
	} 

	if (!mute) {
	    newl = doSetLevel(ll);
	    if (newl != level) {
		level = newl;
		sendEvent(PlayerListener.VOLUME_CHANGED, this);
	    }
	}
	return level;
    }

    /**
     * Get the current volume set for this
     * player.
     *
     * @return The volume in the level scale (0-100).
     */
    public int getLevel() {
	return level;
    }

    /**
     * ================
     * Input functions.
     * ================
     */
    /**
     * the source input stream of this player
     */
    protected InputStream stream;
    /** 
     * the current position in source stream
     */
    long location;

    /**
     * Set the locator of this player.
     *
     * @param locator the locator to be set.
     * @param con the flag if to make the connection
     *
     */
    public void setLocator(String locator, boolean con) throws IOException, 
    MediaException {
	this.locator = locator;
	if (con)
	    openConnection();
    }

    /**
     * Set the input stream of this player.
     *
     * @param stream the input stream to be set.
     *
     */
    public void setStrm(InputStream stream) {
	this.stream = stream;
    }

    /**
     * establish the connection with the source.
     *
     */
    private void openConnection() throws IOException, MediaException {
	try {
	    HttpConnection httpCon = (HttpConnection)Connector.open(locator);
	    int rescode = httpCon.getResponseCode();
	    // both 4XX and 5XX are error codes
	    if (rescode >= 400) {
		httpCon.close();
		throw new IOException("bad url");
	    } else {
		stream = httpCon.openInputStream();
		String ctype = httpCon.getType();
		boolean supportedCT = false;
		if (locator.endsWith(".wav")) {
		    supportedCT = true;
		} else if (ctype != null && 
			   ctype.toLowerCase().equals("audio/x-wav")) {
		    supportedCT = true;
		}
		    
		httpCon.close();

		if (!supportedCT) {
		    stream.close();
		    stream =  null;
		    throw new MediaException("unsupported media type");
		}
	    }
	} catch (IOException ioex) {
	    throw ioex;
	} catch (MediaException mex) {
	    throw mex;
	} catch (Exception ex) {
	    new IOException(ex.getMessage() + " failed to connect");
	}

	location = 0;
    }

    /**
     * Read a data buffer from source stream.
     *
     * @param buffer the byte array to hold the read data
     * @param offset the offset of byte array.
     * @param length the maximum bytes to be read
     * @return the actual number of bytes have been read
     */
    protected int readStrm(byte buffer[], int offset, int length) 
	throws IOException {
	int len = stream.read(buffer, offset, length);
	if (len > 0)
	    location += len;
	return len;
    }

    /**
     * In source stream, seek to a particular position
     * 
     * @param where the position intended to seek to.
     * @return the actual position seeked to.
     * @exception an error occurs during the seeking
     */

    protected long seekStrm(long where) throws IOException, MediaException {
	if (stream == null)
	    return location;
	long skipped, oldLocation = location;
	if (where < oldLocation) { // seek backward
	    reopenStrm();
	    location = 0;
	    skipped = stream.skip(where);
	} else  {
	    skipped = stream.skip((where - oldLocation));
	}

	if (skipped > 0)
	    location += skipped;

	return location;
    }

    /**
     * This is a skip fully method 
     * 
     * @param numBytes the number of bytes intended to skip.
     * @return the actual number of bytes has been skipped.
     * @exception if an error occurs or skipped bytes is less
     * then the intended numBytes.
     */
    protected long skipStrm(int numBytes) throws IOException {
	long skipped = stream.skip(numBytes);
	if (skipped > 0)
	    location += skipped;
	if (skipped < numBytes)
	    throw new IOException("skipped over eom");
	return (skipped);
    }

    /**
     * Re-open the source stream.
     *
     * @exception if an error occurs.
     */
    private void reopenStrm() throws IOException, MediaException {
	try {
	    stream.reset();
	    return;
	} catch (IOException ex) {
	    if (locator == null)
		throw ex;
	}

	try {
	    stream.close();
	    stream = null;
	} catch (IOException e) {}

	
	openConnection();
    }

    /**
     * Get the current position of the source stream
     *
     * @return the current position.
     */
    protected long getStrmLoc() {
	return location;
    }
}


/**
 * The thread that's responsible for delivering Player events.
 * This class lives for only 5 secs.  If no event comes in
 * 5 secs, it will exit.
 */
class EvtQ extends Thread {
    /**
     * the player instance
     */
    private BasicPlayer p;
    /**
     * event type array
     */
    private String[] evtQ;
    /**
     * event data array
     */
    private Object[] evtDataQ;
    /**
     * head and tail pointer of the event queue
     */
    private int head, tail;
    /**
     * the default size of the event queue
     */
    private static final int size = 12;

    /**
     * The constructor
     *
     * @param p the instance of BasicPlayer intending to post event to
     *        this event queue.
     */
    EvtQ(BasicPlayer p) {
	this.p = p;
	evtQ = new String[size];
	evtDataQ = new Object[size];
	start();
    }

    /**
     * Put an event in the event queue and wake up the thread to
     * deliver it.  If the event queue is filled, block.
     *
     * @param evt the evt type
     * @param evtData the event data
     */
    synchronized void sendEvent(String evt, Object evtData) {
	// Wait if the event queue is full.
	// This potentially will block the Player's main thread.
	while ((head + 1) % size == tail) {
	    try {
		wait();
	    } catch (Exception e) { }
	}
	evtQ[head] = evt;
	evtDataQ[head] = evtData;
	if (++head == size)
	    head = 0;
	notify();
    }

    /**
     * the run method for interface Runnable
     */
    public void run() {

	String evt = "";
	Object evtData = null;
	boolean evtToGo = false;

	for (;;) {

	    synchronized (this) {

		// If the queue is empty, we'll wait for at most
		// 5 secs.
		if (head == tail) {
		    try {
			wait(5000);
		    } catch (Exception e) {
		    }
		} 

		if (head != tail) { 
		    evt = evtQ[tail];
		    evtData = evtDataQ[tail];
		    // For garbage collection.
		    evtDataQ[tail] = null;
		    evtToGo = true;
		    if (++tail == size)
			tail = 0;
		    notify();
		} else
		    evtToGo = false;

	    } // synchronized this

	    if (evtToGo) {
		if (evt == PlayerListener.END_OF_MEDIA) {
		    synchronized (p) {
			p.EOM = true;
			p.loopAfterEOM = false;
			if (p.state > Player.PREFETCHED) {
			    p.state = Player.PREFETCHED;
			    if (p.loopCount > 1 || p.loopCount == -1) {
				p.loopAfterEOM = true;
			    }
			}
		    }
                }

		synchronized (p.listeners) {
		    PlayerListener l;
		    for (int i = 0; i < p.listeners.size(); i++) {
			try {
			    l = (PlayerListener)p.listeners.elementAt(i);
			    l.playerUpdate(p, evt, evtData);
			} catch (Exception e) {
			    System.err.println("Error in playerUpdate: " + e);
		        }
		    }
		}

		if (p.loopAfterEOM) 
		    p.doLoop();
	    }
	    
	    if (!evtToGo || evt == PlayerListener.CLOSED) {
		// If there's no event waking up after 5 secs,
		// we'll kill the thread.
		synchronized (p.evtLock) {
		    p.evtQ = null;
		    break;
		}
	    }
	}
    }
}