/*
 * @(#)PlaybackEngine.java	1.16 02/08/21
 *
 * Copyright (c) 1996-2002 Sun Microsystems, Inc.  All rights reserved.
 */

package com.sun.media;

import java.io.*;
import java.util.*;
import java.awt.*;

import javax.media.*;
import javax.media.protocol.*;
import javax.media.control.*;
import javax.media.format.*;
import javax.media.renderer.*;
import com.sun.media.util.*;
import com.sun.media.controls.*;
import com.sun.media.renderer.audio.AudioRenderer;
import com.sun.media.codec.video.colorspace.RGBScaler;


/**
 * PlaybackEngine implements the media engine for playback.
 */
public class PlaybackEngine extends BasicController implements ModuleListener {

    protected BasicPlayer player;
    protected DataSource dsource;
    protected Vector modules;
    protected Vector filters;
    protected Vector sinks;
    protected Vector waitPrefetched;
    protected Vector waitStopped;
    protected Vector waitEnded;
    protected Vector waitResetted;
    protected Track tracks[];
    protected Demultiplexer parser;
    protected BasicSinkModule masterSink = null;
    protected BasicSourceModule source;
    protected SlaveClock slaveClock;

    private boolean internalErrorOccurred = false;
    protected boolean prefetched = false;
    protected boolean started = false;
    private boolean dataPathBlocked = false;
    private boolean useMoreRenderBuffer = false;
    private boolean deallocated = false; 
    public boolean prefetchEnabled = true;
    static protected boolean needSavingDB = false;

    private Time timeBeforeAbortPrefetch = null;
    private float rate = 1.0f;
    protected BitRateControl bitRateControl;
    protected FrameRateControl frameRateControl;
    protected FramePositioningControl framePositioningControl = null;
    private long latency = 0;

    static  boolean DEBUG = true;
    protected JMD jmd = null;
    protected Container container = null;
    
    static public boolean TRACE_ON = false;

    static {
	try {
	    Toolkit.getDefaultToolkit();
	} catch (Throwable t) {
	    DEBUG = false;
	}
    }

    // Controls.
    protected BasicTrackControl trackControls[] = new BasicTrackControl[0];
    protected ProgressControl progressControl;

    private long realizeTime;
    private long prefetchTime;

    // Error messages.
    static String NOT_CONFIGURED_ERROR = "cannot be called before configured";
    static String NOT_REALIZED_ERROR = "cannot be called before realized";
    static String STARTED_ERROR = "cannot be called after started";


    /**
     * Turn on memory trace to assit debugging.
     */
    static public void setMemoryTrace(boolean on) {
	TRACE_ON = on;
    }


    public PlaybackEngine(BasicPlayer p) {
	long initTime = System.currentTimeMillis();

	player = p;
	createProgressControl();

	setClock(slaveClock = new SlaveClock());
	stopThreadEnabled = false;

	profile("instantiation", initTime);
    }



    /**
     * The PlaybackEngine is configurable.
     */
    protected boolean isConfigurable() {
	return true;
    }


    /**
     *  Verifies to see if the engine accepts the given source.
     */
    public void setSource(DataSource ds) throws IOException, 
	IncompatibleSourceException {

	try {
	    source = BasicSourceModule.createModule(ds);
	} catch (IOException ioe) {
	    Log.warning("Input DataSource: " + ds);
	    Log.warning("  Failed with IO exception: " + ioe.getMessage());
	    throw ioe;
	} catch (IncompatibleSourceException ise) {
	    Log.warning("Input DataSource: " + ds);
	    Log.warning("  is not compatible with the MediaEngine.");
	    Log.warning("  It's likely that the DataSource is required to extend PullDataSource;");
	    Log.warning("  and that its source streams implement the Seekable interface ");
	    Log.warning("  and with random access capability.");
	    throw ise;
	}

	if (source == null)
	    throw new IncompatibleSourceException();

	// Create the debugger control
	if (DEBUG && jmd == null) {
	    String jmdTitle = "PlugIn Viewer";
	    if (ds != null && ds.getLocator() != null) {

		jmdTitle = ds.getLocator().toString();

		// We'll determine here if we want to allocate a
		// render buffer for smoother playback.
		// Doing that will increase the latency, so we'll
		// only do that for a few well-known protocols.
		String protocol = ds.getLocator().getProtocol();
		if (protocol != null) {
		    protocol = protocol.toLowerCase();
		    if (protocol.equals("file") ||
			protocol.startsWith("http") ||
			protocol.equals("ftp")) {
			useMoreRenderBuffer = true;
		    }
		}
	    }

	    jmd = new BasicJMD(jmdTitle);
	}
	
	if (DEBUG) source.setJMD(jmd);
	source.setController(this);
	dsource = ds;

	// If it's RTP data source, we'll disable prefetch and reset.
	if (dsource instanceof com.sun.media.protocol.Streamable &&
	    !((com.sun.media.protocol.Streamable)dsource).isPrefetchable()) {
	    prefetchEnabled = false;
	    dataPathBlocked = true;
	}

	if (dsource instanceof CaptureDevice)
	    prefetchEnabled = false;

    }


    String configError = "Failed to configure: " + this;
    String configIntError = "  The configure process is being interrupted.\n";
    String configInt2Error = "interrupted while the Processor is being configured.";
    String parseError = "failed to parse the input media.";

    /**
     * Configuring the engine.
     */
    protected boolean doConfigure() {

	if (!doConfigure1())
	    return false;

	// The indices to the connector names, tracks, and track controls
	// should all correspond to each other.
	String names[] = source.getOutputConnectorNames();
	trackControls = new BasicTrackControl[tracks.length];
	for (int i = 0; i < tracks.length; i++) {
	    trackControls[i] = new PlayerTControl(this, tracks[i], 
				source.getOutputConnector(names[i]));
	}

	return doConfigure2();
    }


    /**
     * Configure - Part I.
     */
    protected boolean doConfigure1() {

	long parsingTime = System.currentTimeMillis();

	modules = new Vector();
        filters = new Vector();
	sinks = new Vector();
	waitPrefetched = new Vector();
	waitStopped = new Vector();
	waitEnded = new Vector();
	waitResetted = new Vector();

	source.setModuleListener(this);
	source.setController(this);
	modules.addElement(source);

	// Realize the source.
	if (!source.doRealize()) {
	    Log.error(configError);
	    if (source.errMsg != null)
		Log.error("  " + source.errMsg + "\n");
	    player.processError = parseError;
	    return false;
	}

	// Check for interrupt after a critical section.
 	if (isInterrupted()) {
	    Log.error(configError);
	    Log.error(configIntError);
	    player.processError = configInt2Error;
	    return false;
	}

	if ((parser = source.getDemultiplexer()) == null) {
	    Log.error(configError);
	    Log.error("  Cannot obtain demultiplexer for the source.\n");
	    player.processError = parseError;
	    return false;
	}

	// Build the track controls.
	try {
	    tracks = parser.getTracks();
	} catch (Exception e) {
	    Log.error(configError);
	    Log.error("  Cannot obtain tracks from the demultiplexer: " + e + "\n");
	    player.processError = parseError;
	    return false;
	}

	// Check for interrupt after a critical section.
 	if (isInterrupted()) {
	    Log.error(configError);
	    Log.error(configIntError);
	    player.processError = configInt2Error;
	    return false;
	}

	profile("parsing", parsingTime);

	return true;
    } 


    /**
     * Configure - Part II.
     */
    protected boolean doConfigure2() {

	if (parser.isPositionable() && parser.isRandomAccess()) {
	    Track master = FramePositioningAdapter.getMasterTrack(tracks);
	    if (master != null) {
		framePositioningControl = new FramePositioningAdapter(player, master);
	    }
	}

	return true;
    }


    protected String realizeError = "Failed to realize: " + this;
    protected String timeBaseError = "  Cannot manage the different time bases.\n";
    protected String genericProcessorError = "cannot handle the customized options set on the Processor.\nCheck jmf.log for full details.";

    /**
     * @return true if successful.
     */ 
    protected synchronized boolean doRealize() {
	return doRealize1() && doRealize2();
    }


    /**
     * doRealize Part I
     */
    protected boolean doRealize1() {

	Log.comment("Building flow graph for: " + dsource.getLocator() + "\n");

	realizeTime = System.currentTimeMillis();

	boolean atLeastOneTrack = false;
	int trackID = 0;
	int numTracks = getNumTracks();

	// Go thru each track to build the graph.
	for (int i = 0; i < trackControls.length; i++) {
	    // Skip the track if its disabled
	    if (!trackControls[i].isEnabled())
		continue;
	    
	    Log.setIndent(0);
	    Log.comment("Building Track: " + i);

	    if (trackControls[i].buildTrack(trackID, numTracks)) {
	        // We've completed at least one track now.
	        atLeastOneTrack = true;
		trackControls[i].setEnabled(true);
	    } else if (trackControls[i].isCustomized()) {
		// If the track is being customized and we cannot
		// build a graph for that track, we won't attempt to
		// build the rest of the tracks.
		Log.error(realizeError);
		trackControls[i].prError();
		player.processError = genericProcessorError;
		return false;
	    } else {
		// Disable that track
		trackControls[i].setEnabled(false);
		Log.warning("Failed to handle track " + i);
		trackControls[i].prError();
	    }

	    // Check for interrupt after a critcial section.
	    if (isInterrupted()) {
		Log.error(realizeError);
		Log.error("  The graph building process is being interrupted.\n");
		player.processError = "interrupted while the player is being constructed.";
		return false;
	    }

	    trackID++;

	    Log.write("\n");
	}

	// Failed if none of the tracks worked. 
	if (!atLeastOneTrack) {
	    Log.error(realizeError);
	    player.processError = "input media not supported: " + getCodecList();
	    return false;
	}

	return true;
    }


    /**
     * doRealize Part II
     */
    protected boolean doRealize2() {

	// Locate the set the master time base.
	if (!manageTimeBases()) {
	    Log.error(realizeError);
	    Log.error(timeBaseError);
	    player.processError = timeBaseError;
	    return false;
	}

	// For debugging
	Log.comment("Here's the completed flow graph:");
	traceGraph(source);
	Log.write("\n");

	profile("graph building", realizeTime);
	realizeTime = System.currentTimeMillis();

	// Update the format info on the progress control.
	updateFormats();

	// For Java Media Debugger
	if (DEBUG) jmd.initGraph(source);

	profile("realize, post graph building", realizeTime);

	return true;
    }


    String getCodecList() {
	String list = "";
	Format fmt;
	for (int i = 0; i < trackControls.length; i++) {
	    fmt = trackControls[i].getOriginalFormat();
	    if (fmt == null || fmt.getEncoding() == null)
		continue;

	    list += fmt.getEncoding();
	    if (fmt instanceof VideoFormat)
		list += " video";
	    else if (fmt instanceof AudioFormat)
		list += " audio";
	    if (i + 1 < trackControls.length)
		list += ", ";
	}
	return list;
    }


    int getNumTracks() {
	int num = 0;
	for (int i = 0; i < trackControls.length; i++) {
	    if (trackControls[i].isEnabled())
		num++;
	}
	return num;
    }
	    

    /**
     * Search and update the master time base.
     */
    boolean manageTimeBases() {

	// Obtain a master time base from one of its SinkModules.
	masterSink = findMasterSink();

	return updateMasterTimeBase();

    }


    /**
     * Returns the DataSink which holds the timebase.
     */
    protected BasicSinkModule findMasterSink() {
	// Search for all the enabled track for a master time base.

	for (int i = 0; i < trackControls.length; i++) {

	    if (!trackControls[i].isEnabled())
		continue;

	    if (trackControls[i].rendererModule != null &&
		trackControls[i].rendererModule.getClock() != null) {
		return trackControls[i].rendererModule;
	    }
	}

	return null;
    }


    /**
     * Update the master timebase on all the modules.
     */
    boolean updateMasterTimeBase() {

	BasicSinkModule bsm;
	int size = sinks.size();

	if (masterSink != null)
	    slaveClock.setMaster(masterSink.getClock());
	else
	    // No master sink found in the module, we'll use the
	    // system time base.
	    slaveClock.setMaster(null);

	// Set the master time base to each of the SinkModules.
	for (int i = 0; i < size; i++) {
	    bsm = (BasicSinkModule)sinks.elementAt(i);
	    if (bsm != masterSink && !bsm.prefetchFailed()) {
		try {
		    bsm.setTimeBase(slaveClock.getTimeBase());
		} catch (IncompatibleTimeBaseException e) {
		    return false;
		}
	    }
	}

	return true;
    }


    /**
     * Called when doConfigure() is aborted.
     */
    protected synchronized void abortConfigure() {
	// The parser could be in the middle of realizing.  We'll need to
	// abort it.
	if (source != null)
	    source.abortRealize();
    }


    /**
     * Called when the realize() is aborted, i.e. deallocate() was called
     * while realizing.  Release all resources claimed previously by the
     * realize() call.
     */
    protected synchronized void abortRealize() {
	StateTransistor m;
	int size = modules.size();
	for (int i = 0; i < size; i++) {
	    m = (StateTransistor)modules.elementAt(i);
	    m.abortRealize();
	}
    }


    /**
     * Called when realize() has failed.
     */
    protected synchronized void doFailedRealize() {
	StateTransistor m;
	int size = modules.size();
	for (int i = 0; i < size; i++) {
	    m = (StateTransistor)modules.elementAt(i);
	    m.doFailedRealize();
	}
	super.doFailedRealize();
    }

    String prefetchError = "Failed to prefetch: " + this;

    /**
     * The stub function to perform the steps to prefetch the controller.
     * @return true if successful.
     */ 
    protected synchronized boolean doPrefetch() {

	if (prefetched)
	    return true;

	return doPrefetch1() && doPrefetch2();
    }


    /**
     * doPrefetch - Part I
     */
    protected boolean doPrefetch1() {

	// If the engine was deallocated before this prefetch, we'll
	// need to sync up the media at the time when the prefetch
	// happens.
	if (timeBeforeAbortPrefetch != null) {
	    doSetMediaTime(timeBeforeAbortPrefetch);
	    timeBeforeAbortPrefetch = null;
	}

	prefetchTime = System.currentTimeMillis();

	// Then prefetch the data path by starting the source 
	// module without starting the renderer modules. 
	resetPrefetchedList();

	// First prefetch each modules.

	// Fail if source cannot be prefetched.
	if (!source.doPrefetch()) {
	    Log.error(prefetchError);
	    if (dsource != null)
		Log.error("  Cannot prefetch the source: " + dsource.getLocator() + "\n"); 
	    return false;
	}

	// Prefetch each track.
	boolean atLeastOneTrack = false;
	boolean usedToFailed;
	for (int i = 0; i < trackControls.length; i++) {

	    usedToFailed = trackControls[i].prefetchFailed;

	    // If this track used to fail and we haven't been
	    // deallocated, then we just skip the track.
	    if (usedToFailed && getState() > Prefetching)
		continue;

	    if (trackControls[i].prefetchTrack()) {
		atLeastOneTrack = true;
		if (usedToFailed) {
		    if (!manageTimeBases()) {
			Log.error(prefetchError);
			Log.error(timeBaseError);
			return false;
		    }
		    // Sync up all the tracks with the current media time
		    // since a new track is added.
		    doSetMediaTime(getMediaTime());
		}

	    } else {

		trackControls[i].prError();

		// If the failed track contain the time base,
		// we'll need to choose a new time base.
		if (trackControls[i].isTimeBase()) {

		    // Fails if no new time base can be found.
		    // This should be rare.
		    if (!manageTimeBases()) {
			Log.error(prefetchError);
			Log.error(timeBaseError);
			player.processError = timeBaseError;
			return false;
		    }
		}

		if (trackControls[i].getFormat() instanceof AudioFormat &&
		    trackControls[i].rendererFailed) {
		    player.processError = "cannot open the audio device.";
		}
	    }
	}

	// Fail if not even one track can be prefetched.
	if (!atLeastOneTrack) {
	    Log.error(prefetchError);
	    return false;
	}

	player.processError = null;

	return true;
    }


    /**
     * doPrefetch - Part II
     */
    protected boolean doPrefetch2() {

	if (prefetchEnabled) {

	    // Wait for notification from the renderer modules with the 
	    // bufferPrefetched event.
	    synchronized (waitPrefetched) {
		source.doStart();
		try {
		    if (!waitPrefetched.isEmpty()) {
			// Block for at most 3 sec. in case anything goes wrong
			// at the renderer modules.
			waitPrefetched.wait(3000);
		    }
		} catch (InterruptedException e) {}
	    }

	} else
	    prefetched = true;

	deallocated = false;

	return true;
    }


    /**
     * Called when the prefetch() is aborted, i.e. deallocate() was called
     * while prefetching.  Release all resources claimed previously by the
     * prefetch call.
     * Override this to implement subclass behavior.
     */
    protected synchronized void abortPrefetch() {
	// Mark the time before the prefetch.  At the next prefetch,
	// we'll sync up the media with this time.
	timeBeforeAbortPrefetch = getMediaTime();
	doReset();
	StateTransistor m;
	int size = modules.size();
	for (int i = 0; i < size; i++) {
	    m = (StateTransistor)modules.elementAt(i);
	    m.abortPrefetch();
	}
	deallocated = true;
    }


    /**
     * Called when the prefetch() has failed.
     */
    protected synchronized void doFailedPrefetch() {
	StateTransistor m;
	int size = modules.size();
	for (int i = 0; i < size; i++) {
	    m = (StateTransistor)modules.elementAt(i);
	    m.doFailedPrefetch();
	}
	super.doFailedPrefetch();
    }


    /**
     * Start immediately.
     * Invoked from start(tbt) when the scheduled start time is reached.
     * Use the public start(tbt) method for the public interface.
     * Override this to implement subclass behavior.
     */
    protected synchronized void doStart() {

	if (started) return;

	doStart1();
	doStart2();
    }


    /**
     * doStart - Part I.
     */
    protected void doStart1() {

	// If the data source is a capture device, we would like
	// to flush the data source before starting. 
	if (dsource instanceof CaptureDevice || isRTP())
	    reset();

	resetPrefetchedList();
	resetStoppedList();
	resetEndedList();

	for (int i = 0; i < trackControls.length; i++) {
	    if (trackControls[i].isEnabled())
		trackControls[i].startTrack();
	}
    }


    /**
     * doStart - Part II
     */
    protected void doStart2() {
	source.doStart();
	started = true;
	prefetched = true;
    }


    /**
     * This is stop by request.
     */
    public synchronized void stop() {
	super.stop();
	sendEvent(new StopByRequestEvent(this, Started,
				  Prefetched,
				  getTargetState(),
				  getMediaTime()));
    }


    protected synchronized void localStop() {
	super.stop();
    }


    /**
     * Invoked from stop().
     * Override this to implement subclass behavior.
     */
    protected synchronized void doStop() {
	if (!started) return;

	doStop1();
	doStop2();
    }


    /**
     * doStop - Part I.
     */
    protected void doStop1() {

	resetPrefetchedList();

	source.doStop();

	for (int i = 0; i < trackControls.length; i++) {
	    if (trackControls[i].isEnabled())
		trackControls[i].stopTrack();
	}
    }


    /**
     * doStop - Part II.
     */
    protected void doStop2() {

	// If prefetching is disabled, we'll also need to do a
	// non-blocking stop on the source since stop is normally
	// called when data is prefetched.
	if (!prefetchEnabled)
	    source.pause();

	started = false;
    }


    /**
     * Override BasicController.setStopTime to allow for more
     * accurate stop time set.
     */
    public void setStopTime(Time t) {
	if (getState() < Realized)
	    throwError(new NotRealizedError("Cannot set stop time on an unrealized controller."));

	if ( getStopTime() != null && 
	     getStopTime().getNanoseconds() != t.getNanoseconds() )
	    sendEvent(new StopTimeChangeEvent(this, t));

	if (getState() == Started && 
	    t != Clock.RESET && t.getNanoseconds() < getMediaNanoseconds()) {
	    // We have already passed the stop time.
	    localStop();
	    setStopTime(Clock.RESET);
	    sendEvent(new StopAtTimeEvent(this, getState(),
					       Prefetched,
					       getTargetState(),
					       getMediaTime()));
	} else {
	    getClock().setStopTime(t);

	    int size = sinks.size();
	    BasicSinkModule bsm;
	    for (int i = 0; i < size; i++) {
		bsm = (BasicSinkModule)sinks.elementAt(i);
		bsm.setStopTime(t);
	    }
	}
    }


    /**
     * Called by deallocate().
     * Subclasses should implement this for its specific behavior.
     */
    protected void doDeallocate() {
    } 


    /**
     * Invoked by close() to cleanup the Controller.
     * Override this to implement subclass behavior.
     */
    protected synchronized void doClose() {
	if (modules == null) {
	    if (source != null) 
		source.doClose();
	    return;
	}
	if (getState() == Started)
	    localStop();
	if (getState() == Prefetched)
	    doReset();
	StateTransistor m;
	int size = modules.size();
	for (int i = 0; i < size; i++) {
	    m = (StateTransistor)modules.elementAt(i);
	    m.doClose();
	}

	if (needSavingDB) {
	    Resource.saveDB();
	    needSavingDB = false;
	}
    }


    RTPInfo rtpInfo = null;
    boolean testedRTP = false;

    public boolean isRTP() {

	if (testedRTP)
	    return rtpInfo != null;

	rtpInfo = (RTPInfo)dsource.getControl("com.sun.media.util.RTPInfo");
	testedRTP = true;

	return rtpInfo != null;
    }


    public String getCNAME() {

	if (rtpInfo == null) {
	    if ((rtpInfo = (RTPInfo)dsource.getControl("com.sun.media.util.RTPInfo")) == null)
		return null;
	}

	return rtpInfo.getCNAME();
    }


    /**
     * Override BasicController's setMediaTime so as not to set the
     * media time on the master clock twice.
     */
    public synchronized void setMediaTime(Time when) {
	if (state < Realized)
	    throwError(new NotRealizedError("Cannot set media time on a unrealized controller"));

	// Chances of this are really rare.  Nevertheless, we'll guard
	// against it.
	if (when.getNanoseconds() == getMediaNanoseconds())
	    return;

	// Reset everything.
	reset();

	// In case deallocate was called, we need to
	// clear the timeBeforeAbortPrefetch flag since we are
	// setting a new time.
	timeBeforeAbortPrefetch = null;

	doSetMediaTime(when);

	// Prefetch the engine again to display the first frame.
	doPrefetch();

	sendEvent(new MediaTimeSetEvent(this, when));
    }


    protected void doSetMediaTime(Time when) {

	slaveClock.setMediaTime(when);

	// Set media time on the renderer modules and the source.
	// Check the return time from the parser.
	Time t;
	if ((t = source.setPosition(when, 0)) == null)
	    t = when;

	int size = sinks.size();
	BasicSinkModule bsm;
	for (int i = 0; i < size; i++) {
	    bsm = (BasicSinkModule)sinks.elementAt(i);
	    bsm.doSetMediaTime(when);
	    bsm.setPreroll(when.getNanoseconds(), t.getNanoseconds());
	}
    }


    public synchronized float doSetRate(float r) {

	if (r <= 0f)
	    r = 1.0f;

	if (r == rate)
	    return r;

	// If the system clock is in use, we need to set rate
	// on the system clock.  Otherwise, set rate on the
	// master clock and let it determine the actual rate use.
	if (masterSink == null)
	    r = getClock().setRate(r);
	else
	    r = masterSink.doSetRate(r);

	BasicModule m;
	int size = modules.size();
	for (int i = 0; i < size; i++) {
	    m = (BasicModule)modules.elementAt(i);
	    if (m != masterSink)
		m.doSetRate(r);
	}

	rate = r;

	return r;
    }


    /**
     * Flush (reset) the flow graph.
     * This is a blocking call and should only be called when the 
     * engine is stopped.
     */
    protected synchronized void reset() {
	// If a module has been blocked, we'll need 
	// to return from the reset.
	if (started || !prefetched || dataPathBlocked)
	    return;

	doReset();
    }


    /**
     * The real reset code.
     * This is a blocking call and should only be called when the 
     * engine is stopped.
     */
    protected synchronized void doReset() {

	// We'll need to block until the modules are fully resetted to
	// move on.
	synchronized (waitResetted) {

	    resetResettedList();

	    // Put all modules in the reset state.  The source module
	    // is the last to receive the reset call.  It will trigger
	    // zero-length flush buffers to flow from the source to
	    // the sinks.  When the sinks receive the flush buffers,
	    // it will reply back with the resetted callbacks.

	    BasicModule m;
	    int size = modules.size();
	    for (int i = size-1; i >= 0; i--) {
		m = (BasicModule)modules.elementAt(i);
		if (!m.prefetchFailed())
		    m.reset();
	    }

	    // The data flow (including the flush buffers) could be blocked 
	    // at the sinks.  To initiate the reset, we'll call 
	    // triggerReset on the sinks.
	    BasicSinkModule bsm;
	    size = sinks.size();
	    for (int i = 0; i < size; i++) {
		bsm = (BasicSinkModule)sinks.elementAt(i);
		if (!bsm.prefetchFailed())
		    bsm.triggerReset();
	    }

	    if (!waitResetted.isEmpty()) {
		try {
		    // Wait at most 3 seconds in case anything goes
		    // wrong at the plugins.  We have no control on
		    // how plugin writers write their plugin's.
		    waitResetted.wait(3000);
		} catch (Exception e) {}
	    }

	    // Some of the sinks may need to be stop again.
	    size = sinks.size();
	    for (int i = 0; i < size; i++) {
		bsm = (BasicSinkModule)sinks.elementAt(i);
		if (!bsm.prefetchFailed())
		    bsm.doneReset();
	    }
	}

	prefetched = false;
    }


    /**
     * Reset the renderer lists.
     */
    private void resetPrefetchedList() {
	synchronized (waitPrefetched) {
	    waitPrefetched.removeAllElements();
	    int size = sinks.size();
	    BasicSinkModule bsm;
	    for (int i = 0; i < size; i++) {
		bsm = (BasicSinkModule)sinks.elementAt(i);
		if (!bsm.prefetchFailed())
		    waitPrefetched.addElement(bsm);
	    }
	    waitPrefetched.notifyAll();
	}
    }


    /**
     * Reset the renderer lists.
     */
    private void resetStoppedList() {
	synchronized (waitStopped) {
	    waitStopped.removeAllElements();
	    int size = sinks.size();
	    BasicSinkModule bsm;
	    for (int i = 0; i < size; i++) {
		bsm = (BasicSinkModule)sinks.elementAt(i);
		if (!bsm.prefetchFailed())
		    waitStopped.addElement(bsm);
	    }
	    waitStopped.notifyAll();
	}
    }


    /**
     * Reset the renderer lists.
     */
    private void resetEndedList() {
	synchronized (waitEnded) {
	    waitEnded.removeAllElements();
	    int size = sinks.size();
	    BasicSinkModule bsm;
	    for (int i = 0; i < size; i++) {
		bsm = (BasicSinkModule)sinks.elementAt(i);
		if (!bsm.prefetchFailed())
		    waitEnded.addElement(bsm);
	    }
	    waitEnded.notifyAll();
	}
    }


    /**
     * Reset the renderer lists.
     */
    private void resetResettedList() {
	// Set up the wait queue for the reset notifications first.
	// We'll wait for reset note from sinks and the source only.
	synchronized (waitResetted) {
	    waitResetted.removeAllElements();
	    int size = sinks.size();
	    BasicSinkModule bsm;
	    for (int i = 0; i < size; i++) {
		bsm = (BasicSinkModule)sinks.elementAt(i);
		if (!bsm.prefetchFailed())
		    waitResetted.addElement(bsm);
	    }
	    waitResetted.notifyAll();
	}
    }


    boolean prefetchLogged = false;

    public void bufferPrefetched(Module src) {
	if (!prefetchEnabled)
	    return;
	if (src instanceof BasicSinkModule) {
	    synchronized (waitPrefetched) {
		if (waitPrefetched.contains(src))
		    waitPrefetched.removeElement(src);
		if (waitPrefetched.isEmpty()) {
		    // All sinks are prefetched.  Wake up the
		    // doPrefetch() wait.
		    waitPrefetched.notifyAll();

		    if (!prefetchLogged) {
			profile("prefetch", prefetchTime);
			prefetchLogged = true;
		    }

		    if (getState() != Controller.Started &&
			getTargetState() != Controller.Started) {
			// Non-blocking stop.
			source.pause();	
		    }
		    prefetched = true;
		}
	    }
	}
    }


    public void stopAtTime(Module src) {
	if (src instanceof BasicSinkModule) {
	    synchronized (waitStopped) {
		if (waitStopped.contains(src))
		    waitStopped.removeElement(src);

		// Check to see if all the tracks have stopped or
		// all other tracks have reached EOM and this is the
		// only track left behind.
		if (waitStopped.isEmpty() ||
		    waitEnded.size() == 1 && waitEnded.contains(src)) {
		    // Stop at time.
		    started = false;
		    stopControllerOnly();
		    setStopTime(Clock.RESET);
		    sendEvent(new StopAtTimeEvent(this, Started,
					       Prefetched,
					       getTargetState(),
					       getMediaTime()));
		    slaveClock.reset(USE_MASTER);
		} else if (src == masterSink) {
		    // If the master sink has finished but the
		    // not all the other tracks are finished, we'll
		    // need to switch the clock to using a backup for
		    // the rest of the media.
	    	    slaveClock.reset(USE_BACKUP);
		}
	    }
	}
    }


    public void mediaEnded(Module src) {
	if (src instanceof BasicSinkModule) {
	    synchronized (waitEnded) {
		if (waitEnded.contains(src))
		    waitEnded.removeElement(src);
		if (waitEnded.isEmpty()) {
		    // EOM.
		    started = false;
		    stopControllerOnly();
 		    sendEvent(new EndOfMediaEvent(this, Started, 
					Prefetched, getTargetState(), 
					getMediaTime()));
		    slaveClock.reset(USE_MASTER);
		} else if (src == masterSink) {
		    // If the master sink has finished but the
		    // not all the other tracks are finished, we'll
		    // need to switch the clock to using a backup for
		    // the rest of the media.
	    	    slaveClock.reset(USE_BACKUP);
		}
	    }
	}
    }


    public void resetted(Module src) {
	synchronized (waitResetted) {
	    if (waitResetted.contains(src)) {
		waitResetted.removeElement(src);
	    }
	    if (waitResetted.isEmpty()) {
		// Everyone modules has been resetted.
		waitResetted.notifyAll();
	    }
	}
    }


    public void dataBlocked(Module src, boolean blocked) {

	dataPathBlocked = blocked;
  	if (blocked) {
	    // Break the lock for the blocking prefetch & reset.
	    resetPrefetchedList();
	    resetResettedList();
	}

	if ( getTargetState() != Controller.Started) {
	    return;
	}

  	if (blocked) {
	    localStop();
	    setTargetState(Controller.Started); // NOTE: may cause race condition
	    sendEvent( new RestartingEvent(this,
					Controller.Started,
					Controller.Prefetching,
					Controller.Started,
					getMediaTime()));
	} else {
	    sendEvent( new StartEvent(this,
				       Controller.Prefetched,
				       Controller.Started,
				       Controller.Started,
				       getMediaTime(),
				       getTimeBase().getTime()));
	}
    }


    public void framesBehind(Module src, float frames, InputConnector ic) {

	OutputConnector oc;
	BasicFilterModule bfm;

	while (ic != null) {
	    if ((oc = ic.getOutputConnector()) == null)
		break;
	    if ((src = oc.getModule()) == null)
		break;
	    if (!(src instanceof BasicFilterModule))
		break;
	    bfm = (BasicFilterModule)src;
	    bfm.setFramesBehind(frames);
	    ic = src.getInputConnector(null);
	}
    }


    long markedDataStartTime = 0;
    boolean reportOnce = false;
    public void markedDataArrived(Module src, Buffer buffer) {
	if (src instanceof BasicSourceModule) {
	    markedDataStartTime = getMediaNanoseconds();
	} else {
	    long t = getMediaNanoseconds() - markedDataStartTime;
	    if (t > 0 && t < 1000000000) {
		if (!reportOnce) {
		    Log.comment("Computed latency for video: " + t/1000000 + " ms\n");
		    reportOnce = true;
		}
		latency = (t + latency)/2;
	    }
	}
    }


    public void formatChanged(Module src, Format oldFormat, Format newFormat) {
	Log.comment(src + ": input format changed: " + newFormat);

	// Handle size change independently.
	if (src instanceof BasicRendererModule && 
	    oldFormat instanceof VideoFormat && 
	    newFormat instanceof VideoFormat) {
	    Dimension s1 = ((VideoFormat)oldFormat).getSize();
	    Dimension s2 = ((VideoFormat)newFormat).getSize();
	    if (s2 != null && (s1 == null || !s1.equals(s2))) {
		sendEvent(new SizeChangeEvent(this, s2.width, s2.height, 1.0f));
	    }
	}
    }


    public void formatChangedFailure(Module src, Format oldFormat, Format newFormat) {
	// Send an internal error event for now.
	// Future: need to rebuild the graph.
	// -ivg
	if (!internalErrorOccurred) {
	    sendEvent(new InternalErrorEvent(this, "Internal module " + src + ": failed to handle a data format change!")); 
	    internalErrorOccurred = true;
	    close();
	}
    }


    public void pluginTerminated(Module src) {
	if (!internalErrorOccurred) {
	    sendEvent(new ControllerClosedEvent(this)); 
	    internalErrorOccurred = true;
	    close();
	}
    }


    public void internalErrorOccurred(Module src) {
	if (!internalErrorOccurred) {
	    sendEvent(new InternalErrorEvent(this, "Internal module " + src + " failed!")); 
	    internalErrorOccurred = true;
	    close();
	}
    }


    /**
     * Return true if audio is present.
     */
    public boolean audioEnabled() {
	for (int i = 0; i < trackControls.length; i++) {
	    if (trackControls[i].isEnabled() &&
		trackControls[i].getOriginalFormat() instanceof AudioFormat)
		return true;
	}
	return false;
    }


    /**
     * Return true if video is present.
     */
    public boolean videoEnabled() {
	for (int i = 0; i < trackControls.length; i++) {
	    if (trackControls[i].isEnabled() &&
		trackControls[i].getOriginalFormat() instanceof VideoFormat)
		return true;
	}
	return false;
    }


    /**
     * Return a list of <b>Control</b> objects this <b>Controller</b>
     * supports.
     * In this case, it is all the controls from all the modules
     * controlled by this engine.
     *
     * @return list of <b>Controller</b> controls.
     */
    public Control[] getControls() {

	// build the list of controls.  It is the total of all the
	// controls from each module plus the ones that are maintained
	// by the engine itself (e.g. progressControl).

	Control controls[];
	Vector cv = new Vector();
	Control c;
	Object cs[];
	Module m;
	int i, size = (modules == null ? 0 : modules.size());
	int otherSize = 0;

	// Collect controls from all the modules.
	for (i = 0; i < size; i++) {
	    m = (Module)modules.elementAt(i);
	    cs = m.getControls();
	    if (cs == null) continue;
	    for (int j = 0; j < cs.length; j++) {
		cv.addElement(cs[j]);
	    }
	}

	size = cv.size();

	// Controls owned by the engine itself.

	if (videoEnabled()) {
	    if (frameRateControl == null) {
		frameRateControl = new FrameRateAdapter(player, 0f, 0f, 30f, false) {
		    
		    public float setFrameRate(float rate) {
			this.value = rate;
			return -1f;
		    }
		    
		    public Component getControlComponent() {
			return null;
		    }

		    public Object getOwner() {
			return player;
		    }
		};
	    }
	}

	if (bitRateControl == null) {
	    bitRateControl = new BitRateA(0, -1, -1, false);
	}	
	if (frameRateControl != null)
	    otherSize++;

	if (bitRateControl != null)
	    otherSize++;

	if (framePositioningControl != null)
	    otherSize++;
	
	if (DEBUG) otherSize++;

	controls = new Control[size + otherSize + trackControls.length];

	for (i = 0; i < size; i++)
	    controls[i] = (Control)cv.elementAt(i);

	if (bitRateControl != null)
	    controls[size++] = bitRateControl;

	if (frameRateControl != null)
	    controls[size++] = frameRateControl;

	if (framePositioningControl != null)
	    controls[size++] = framePositioningControl;

	if (DEBUG)
	    controls[size++] = jmd;
	
	// and the tracks
	for (i = 0; i < trackControls.length; i++) {
	    controls[size + i] = trackControls[i];
	}
	
	return controls;
    }


    /**
     * Get audio gain control.
     */
    public GainControl getGainControl() {
	return (GainControl)getControl("javax.media.GainControl");
    }


    /**
     * Get the visual component where the video is presented.
     */
    public Component getVisualComponent() {
	Vector visuals = new Vector(1);
	if (modules == null)
	    return null;
	for (int i = 0; i < modules.size(); i++) {
	    BasicModule bm = (BasicModule) modules.elementAt(i);
	    PlugIn pi = getPlugIn(bm);
	    if (pi instanceof VideoRenderer) {
		Component comp = ((VideoRenderer)pi).getComponent();
		if (comp != null)
		    visuals.addElement(comp);
	    }
	}
	if (visuals.size() == 0)
	    return null;
	else if (visuals.size() == 1)
	    return (Component) visuals.elementAt(0);
	else {
	    // Multiple components, put them into one container
	    return createVisualContainer(visuals);
	}
    }

    protected Component createVisualContainer(Vector visuals) {
	Boolean hint = (Boolean) Manager.getHint(Manager.LIGHTWEIGHT_RENDERER);
	if (container == null) {
	    if (hint == null || hint.booleanValue() == false) {
		container = new HeavyPanel(visuals);
	    } else {
		container = new LightPanel(visuals);
	    }
	
	    container.setLayout( new FlowLayout() );
	    container.setBackground(Color.black);
	    for (int i = 0; i < visuals.size(); i++) {
		Component c = (Component)visuals.elementAt(i);
		container.add(c);
		c.setSize(c.getPreferredSize());
	    }
	}
	return container;
    }
    
    /**
     * Returns the start latency.
     * Don't know until the particular node is implemented.
     * @return the start latency.
     */
    public Time getStartLatency() {
	if ( (state == Unrealized) || (state == Realizing) )
	    throwError(new NotRealizedError("Cannot get start latency from an unrealized controller"));
	return LATENCY_UNKNOWN;
    }


    /**
     * Return the run-time latency.  It's the time it takes for a packet
     * to travel from the first module to the last module.
     * The time is computed in nanoseconds.
     */
    public long getLatency() {
	return latency;
    }


    /**
     * Return the duration of the media.
     * It's unknown until we implement a particular node.
     * @return the duration of the media.
     */
    public Time getDuration() {
	return source.getDuration();
    }
    
    public void setProgressControl(ProgressControl p){
	progressControl = p;
    }
	
    /**
     * Create the progress status control.
     */
    public void createProgressControl() {
	// Build the progress control.
	StringControl frameRate;
	StringControl bitRate;
	StringControl videoProps;
	StringControl audioProps;
	StringControl videoCodec;
	StringControl audioCodec;

        frameRate = new StringControlAdapter();
        frameRate.setValue(JMFI18N.getResource("mediaplayer.N/A"));
        bitRate = new StringControlAdapter();
        bitRate.setValue(JMFI18N.getResource("mediaplayer.N/A"));
        videoProps = new StringControlAdapter();
        videoProps.setValue(JMFI18N.getResource("mediaplayer.N/A"));
        audioProps = new StringControlAdapter();
        audioProps.setValue(JMFI18N.getResource("mediaplayer.N/A"));
        audioCodec = new StringControlAdapter();
        audioCodec.setValue(JMFI18N.getResource("mediaplayer.N/A"));
        videoCodec = new StringControlAdapter();
        videoCodec.setValue(JMFI18N.getResource("mediaplayer.N/A"));
        progressControl = new ProgressControlAdapter( frameRate, 
						      bitRate, 
						      videoProps,
						      audioProps,
						      videoCodec,
						      audioCodec );
    }


    /**
     * Update the format info per track on the progress control.
     */
    public void updateFormats() {
	// Update formats per track.
	for (int i = 0; i < trackControls.length; i++) {
	    trackControls[i].updateFormat();
	}
    }


    long lastBitRate = 0;
    long lastStatsTime = 0;  // so now - lastStatsTime will not be 0.

    /**
     * Update the aggregate bit rate and frame rate per track on 
     * the progress control.
     */
    public void updateRates() {

	if (getState() < Realized)
	    return;

	// Update global bit rate.
	long now = System.currentTimeMillis();
	long rate, avg;

	if (now == lastStatsTime)
	    rate = lastBitRate;
	else
	    rate = (long)((double)getBitRate()*8.0/
				(now - lastStatsTime)*1000.0);
	avg = (lastBitRate + rate)/2;

	if (bitRateControl != null) {
	    bitRateControl.setBitRate((int)avg);
	}
	
	lastBitRate = rate;
	lastStatsTime = now;
	resetBitRate();

	// Update frame rate on a per-track basis.
	for (int i = 0; i < trackControls.length; i++) {
	    trackControls[i].updateRates(now);
	}

	// Compute the instanteous latency.
	source.checkLatency();
    }


    protected long getBitRate() {
	return source.getBitsRead();
    }


    protected void resetBitRate() {
	source.resetBitsRead();
    }


    /**
     * Override the parent's method to not check for realized state.  There's
     * no need to.
     */
    public void setTimeBase(TimeBase tb) throws IncompatibleTimeBaseException {
	getClock().setTimeBase(tb);
	// Set the time base on each of the SinkModules.
	if (sinks == null)
	    return;
	int size = sinks.size();
	BasicSinkModule bsm;
	for (int i = 0; i < size; i++) {
	    bsm = (BasicSinkModule)sinks.elementAt(i);
	    bsm.setTimeBase(tb);
	}
    }

    /**
     * Override the parent's method to not check for realized state.  There's
     * no need to.
     */
    public TimeBase getTimeBase() {
	return getClock().getTimeBase();
    }

    //////////////////////////////////
    //
    // Flow graph building routines.
    //////////////////////////////////


    /**
     * Construct a track (connected modules) from the specified node graph.
     * Return a non-null GraphNode if the track cannot be built.  The
     * non-null node returned is the node that failed to be opened.
     */
    protected GraphNode buildTrackFromGraph(BasicTrackControl tc, 
				GraphNode node) {
	BasicModule src = null, dst = null;
	InputConnector ic = null;
	OutputConnector oc = null;
	boolean lastNode = true;
	Vector used = new Vector(5);
	int indent = 0;

	if (node.plugin == null) {
	    // There's nothing to build.
	    // i.e. the output from the source (demux) works just fine.
	    // Probably just need to be multiplexed.
	    return null;
	}

	Log.setIndent(indent++);

	// Build the graph from the last node.
	while (node != null && node.plugin != null) {

	    if ((src = createModule(node, used)) == null) {
		// something terrible went wrong.
		Log.error("Internal error: buildTrackFromGraph");
		node.failed = true;
		return node;
	    }

	    // Mark the renderer module or the last output connector
	    // from in the trackcontrol.
	    if (lastNode) {
		if (src instanceof BasicRendererModule) {

		    tc.rendererModule = (BasicRendererModule)src;

		    // If we are dealing with an audio renderer here, we'll
		    // set the jitter buffer size.
		    if (useMoreRenderBuffer &&
			tc.rendererModule.getRenderer() instanceof AudioRenderer)
			setRenderBufferSize(tc.rendererModule.getRenderer());

		} else if (src instanceof BasicFilterModule) {
		    tc.lastOC = src.getOutputConnector(null);
		    tc.lastOC.setFormat(node.output);
		}
		lastNode = false;
	    }

	    ic = src.getInputConnector(null);
	    ic.setFormat(node.input);

	    if (dst != null) {
		oc = src.getOutputConnector(null);
		ic = dst.getInputConnector(null);
		oc.setFormat(ic.getFormat());
	    }

	    src.setController(this);
	    if (!src.doRealize()) {
		//Log.write("Failed to open plugin: " + node.plugin);
		//Log.write("  with input: " + node.input);
		Log.setIndent(indent--);
		node.failed = true;
		return node;
	    }

	    if (oc != null && ic != null)
		connectModules(oc, ic, dst);

	    dst = src;
	    node = node.prev;
	}

	// All successful, so we need to add each modules under the engine's
	// management.

	dst = src;
	while (true) {
	    dst.setModuleListener(this);
	    modules.addElement(dst);
	    tc.modules.addElement(dst);
	    if (dst instanceof BasicFilterModule)
		filters.addElement(dst);
	    else if (dst instanceof BasicSinkModule)
		sinks.addElement(dst);
	    oc = dst.getOutputConnector(null);
	    if (oc == null ||
		(ic = oc.getInputConnector()) == null ||
		(dst = (BasicModule)ic.getModule()) == null)
		break;
	}

	// Set the first output connector.
	tc.firstOC.setFormat(tc.getOriginalFormat());

	// Check if the first module has the format set.
	ic = src.getInputConnector(null);
	Format fmt = ic.getFormat();
	if (fmt == null || !fmt.equals(tc.getOriginalFormat()))
	    ic.setFormat(tc.getOriginalFormat());

	connectModules(tc.firstOC, ic, src);

	Log.setIndent(indent--);

	return null;
    }


    protected void setRenderBufferSize(Renderer r) {
	BufferControl bc = (BufferControl)r.getControl("javax.media.control.BufferControl");
	if (bc != null)
	    bc.setBufferLength(2000);
    }


    /**
     * Given a chain of FilterModules, return the last one of the chain.
     */
    protected BasicModule lastModule(BasicModule bm) {
	OutputConnector oc;
	InputConnector ic;

	oc = bm.getOutputConnector(null);
	while (oc != null && (ic = oc.getInputConnector()) != null) {
	    bm = (BasicModule)ic.getModule();
	    oc = bm.getOutputConnector(null);
	}

	return bm;
    }


    /**
     * Create a realized filter module given the plugIn codec.
     */
    protected BasicModule createModule(GraphNode n, Vector used) {
	PlugIn p;
	BasicModule m = null;

	if (n.plugin == null)
	    return null;

	if (used.contains(n.plugin)) {
	    // That plugin has already been used in the same path,
	    // we'll need to instantiate another one of its kind.
	    if (n.cname == null || 
		(p = SimpleGraphBuilder.createPlugIn(n.cname, -1)) == null) {
		Log.write("Failed to instantiate " + n.cname);
		return null;
	    }
	} else {
	    p = n.plugin;
	    used.addElement(p);
	}

	if ((n.type == -1 || n.type == PlugInManager.RENDERER) && 
	    p instanceof Renderer) {
	    m = new BasicRendererModule((Renderer)p);
	} else if ((n.type == -1 || n.type == PlugInManager.CODEC) && 
		   p instanceof Codec) {
	    m = new BasicFilterModule((Codec)p);
	}

	if (DEBUG && m != null) m.setJMD(jmd);

	return m;
    }


    /**
     * Connect the two given modules.
     */
    protected void connectModules(OutputConnector oc, InputConnector ic, BasicModule dst) {
	// If the dst is the renderer, adopt the dest's protocol.
	// Otherwise, adopt the source's protocol.
	if (dst instanceof BasicRendererModule)
	    oc.setProtocol(ic.getProtocol());
	else
	    ic.setProtocol(oc.getProtocol());

	oc.connectTo(ic, ic.getFormat());
    }


    /**
     * Return true if the given format is a raw video format.
     */
    static boolean isRawVideo(Format fmt) {
	return (fmt instanceof RGBFormat || fmt instanceof YUVFormat);
    }


    /**
     * Trace the flow graph for debugging.
     */
    void traceGraph(BasicModule source) {
	Module m;
	OutputConnector oc;
	InputConnector ic;
	String names[];

	names = source.getOutputConnectorNames();
	for (int i = 0; i < names.length; i++) {

	    oc = source.getOutputConnector(names[i]);
	    if ((ic = oc.getInputConnector()) == null)
		continue;
	    if ((m = ic.getModule()) == null)
		continue;
	    Log.write("  " + getPlugIn(source));
	    Log.write("     connects to: " + getPlugIn((BasicModule)m));
	    Log.write("     format: " + oc.getFormat());
	    traceGraph((BasicModule)m);
	}
    }


    /**
     * Get the plugin from a module.  For debugging.
     */
    protected PlugIn getPlugIn(BasicModule m) {

	if (m instanceof BasicSourceModule)
	    return ((BasicSourceModule)m).getDemultiplexer();

	if (m instanceof BasicFilterModule)
	    return ((BasicFilterModule)m).getCodec();

	if (m instanceof BasicRendererModule)
	    return ((BasicRendererModule)m).getRenderer();

	return null;
    }

    static void profile(String msg, long time) {
	Log.profile("Profile: " + msg + ": " + 
		(System.currentTimeMillis() - time) + " ms\n");
    }


    //////////////////////////////////
    //
    // Inner classes
    //////////////////////////////////

    class BitRateA extends BitRateAdapter implements Owned {

	public BitRateA(int initialBitRate, int minBitRate, int maxBitRate,
			boolean settable) {
	    super(initialBitRate, minBitRate, maxBitRate, settable);
	}
	
	public int setBitRate(int rate) {
	    this.value = rate;
	    return this.value;
	}
	
	public Component getControlComponent() {
	    return null;
	}
	
	public Object getOwner() {
	    return player;
	}
    }
    
    static boolean USE_MASTER = true;
    static boolean USE_BACKUP = false;

    class SlaveClock implements Clock {
	
	Clock master, current;
	BasicClock backup;

	SlaveClock() {
	    backup = new BasicClock();
	    current = backup;
	}

	public void setMaster(Clock master) {
	    this.master = master;
	    current = (master == null ? backup : master);
	    if (master != null) {
		try {
		    backup.setTimeBase(master.getTimeBase());
		} catch (IncompatibleTimeBaseException e) {
		}
	    }
	}

    	public void setTimeBase(TimeBase tb) throws 
			IncompatibleTimeBaseException{
	    synchronized (backup) {
		backup.setTimeBase(tb);
	    }
	}

	public void syncStart(Time tbt) {
	    synchronized (backup) {
		if (backup.getState() != BasicClock.STARTED)
		    backup.syncStart(tbt);
	    }
	}

	public void stop() {
	    synchronized (backup) {
		backup.stop();
	    }
	}

	public void setStopTime(Time t) {
	    synchronized (backup) {
		backup.setStopTime(t);
	    }
	}

	public Time getStopTime() {
	    return backup.getStopTime();
	}

	public void setMediaTime(Time now) {
	    synchronized (backup) {
		if (backup.getState() == BasicClock.STARTED) {
		    backup.stop();
		    backup.setMediaTime(now);
		    backup.syncStart(backup.getTimeBase().getTime());
		} else
		    backup.setMediaTime(now);
	    }
	}

	public Time getMediaTime() {
	    return current.getMediaTime();
	}

	public long getMediaNanoseconds() {
	    return current.getMediaNanoseconds();
	}

	public Time getSyncTime() {
	    return current.getSyncTime();
	}

	public TimeBase getTimeBase() {
	    return current.getTimeBase();
	}

	public Time mapToTimeBase(Time t) throws ClockStoppedException {
	    return current.mapToTimeBase(t);
	}

	public float setRate(float factor) {
	    return backup.setRate(factor);
	}

	public float getRate() {
	    return current.getRate();
	}

	protected void reset(boolean useMaster) {
	    if (master != null && useMaster)
		current = master;
	    else {
		// Align the backup clock's media time with the 
		// master's time.
		if (master != null) {
		  synchronized (backup) {
		    boolean started = false;
		    if (backup.getState() == BasicClock.STARTED) {
			backup.stop();
			started = true;
		    }
		    backup.setMediaTime(master.getMediaTime());
		    if (started)
			backup.syncStart(backup.getTimeBase().getTime());
		  }
		}
		current = backup;
	    }
	}
    }


    /**
     * Track Control.
     */
    class PlayerTControl extends BasicTrackControl implements Owned {

	protected PlayerGraphBuilder gb;

	public PlayerTControl(PlaybackEngine engine, Track track, OutputConnector oc) {
	    super(engine, track, oc);
	}

	public Object getOwner() {
	    return player;
	}

	/**
	 * Top level routine to build a single track.
	 */
	public boolean buildTrack(int trackID, int numTracks) {
	    if (gb == null)
		gb = new PlayerGraphBuilder(engine);
	    else
		gb.reset();
	    boolean rtn = gb.buildGraph(this);

	    // dispose the old GraphBuilder after building a track.
	    // The cache is not valid anymore.
	    gb = null;

	    return rtn;
	}


	/**
	 * Returns true if this track holds the master time base.
	 */
	public boolean isTimeBase() {
	    for (int j = 0; j < this.modules.size(); j++) {
		if (this.modules.elementAt(j) == masterSink)
		    return true;
	    }
	    return false;
	}


	protected ProgressControl progressControl() {
	    return progressControl;
	}


	protected FrameRateControl frameRateControl() {
	    return frameRateControl;
	}
    }



    /**
     * This is the Graph builder extended from SimpleGraphBuilder 
     * to generate the data flow graph for the given track.
     */
    class PlayerGraphBuilder extends SimpleGraphBuilder {

	protected PlaybackEngine engine;

	PlayerGraphBuilder(PlaybackEngine engine) {
	    this.engine = engine;
	}

	protected GraphNode buildTrackFromGraph(BasicTrackControl tc, GraphNode node) {
	    return engine.buildTrackFromGraph((PlayerTControl)tc, node);
	}
    }


    class HeavyPanel extends java.awt.Panel implements VisualContainer {
	public HeavyPanel(Vector visuals) {
	}
    }

    class LightPanel extends java.awt.Container implements VisualContainer {
	public LightPanel(Vector visuals) {
	}
    }
}
