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

package com.sun.media;

import java.lang.reflect.Method;
import java.lang.reflect.Constructor;
import javax.media.*;
import javax.media.format.*;
import javax.media.format.AudioFormat;
import javax.media.format.VideoFormat;
import com.sun.media.*;
import com.sun.media.util.*;
import com.sun.media.renderer.audio.AudioRenderer;
import com.sun.media.rtp.util.RTPTimeBase;
import com.sun.media.rtp.util.RTPTimeReporter;

import com.ms.security.PermissionID;
import com.ms.security.PolicyEngine;

/**
 * BasicRenderer is a module which have InputConnectors and no OutputConnectors.
 * It receives data from its input connector and put its output in output device
 * such as file, URL, screen, audio device, output DataSource or null.<br>
 * MediaRenderers can be either Pull driven (as AudioPlayer) or Push
 * driven (as File renderer). VideoRenderer might be implemented as either Push or Pull.<br>
 * MediaRenderers are stopAtTime aware (so that the audio renderer would stop at the correct time) and
 * are responsible to stop the player at the required time (no separate thread for poling TimeBase). <br>
 * There is no need to define buffers allocation and connectors behavior here, as it is done in module. <br><br>
 * <i>Common functionality of renderers would be put here as we start the implementation</i><br>
 * <i>We need the level 3 design to continue working on this class</i>
 *
 **/
public class BasicRendererModule extends BasicSinkModule 
    implements RTPTimeReporter {

    protected PlaybackEngine engine;
    protected Renderer renderer;
    protected InputConnector ic;
    protected int framesPlayed = 0;
    protected float frameRate = 30f;
    protected boolean framesWereBehind = false;
    protected boolean prefetching = false;
    protected boolean started = false;
    private boolean opened = false;	// to avoid opening the plugin more
					// than once.
    private int chunkSize = Integer.MAX_VALUE;
    private long prefetchedAudioDuration = 0;
    private long lastDuration = 0;

    private RTPTimeBase rtpTimeBase = null;

    private String rtpCNAME = null;

    RenderThread renderThread;

    private static JMFSecurity jmfSecurity = null;
    private static boolean securityPrivelege=false;
    private Method m[] = new Method[1];
    private Class cl[] = new Class[1];
    private Object args[][] = new Object[1][0];
    private Object prefetchSync = new Object();

    // This is the media time as computed by the data that's
    // gone through the sink.  This is not the same as the
    // time reported by the Player's clock.  The difference is
    // the player's time also take into consideration the
    // renderer's buffered data.  Hence it reflects the actual
    // time played through the renderer.
    // elapseTime is used for prerolling and setStopTime.
    private ElapseTime elapseTime = new ElapseTime();

    static {
	try {
	    jmfSecurity = JMFSecurityManager.getJMFSecurity();
	    securityPrivelege = true;
	} catch (SecurityException e) {
	}
    }

    protected BasicRendererModule(Renderer r) {
	setRenderer(r);
	ic = new BasicInputConnector();
	if (r instanceof javax.media.renderer.VideoRenderer)
	    ic.setSize(4);
	else
	    ic.setSize(1);
	ic.setModule(this);
	registerInputConnector("input", ic);
	setProtocol(Connector.ProtocolSafe);
    }

    public boolean isThreaded() {
	return true;
    }

    public boolean doRealize() {
	chunkSize = computeChunkSize(ic.getFormat());

	if ( /*securityPrivelege &&*/ (jmfSecurity != null) ) {
	    String permission = null;
	    try {
		if (jmfSecurity.getName().startsWith("jmf-security")) {
		    permission = "thread";
		    jmfSecurity.requestPermission(m, cl, args, JMFSecurity.THREAD);
		    m[0].invoke(cl[0], args[0]);
		    
		    permission = "thread group";
		    jmfSecurity.requestPermission(m, cl, args, JMFSecurity.THREAD_GROUP);
		    m[0].invoke(cl[0], args[0]);
		} else if (jmfSecurity.getName().startsWith("internet")) {
		    PolicyEngine.checkPermission(PermissionID.THREAD);
		    PolicyEngine.assertPermission(PermissionID.THREAD);
		}
	    } catch (Throwable e) {
		if (JMFSecurityManager.DEBUG) {
		    System.err.println("Unable to get " + permission +
				       " privilege  " + e);
		}
		securityPrivelege = false;
		// TODO: Do the right thing if permissions cannot be obtained.
		// User should be notified via an event
	    }
	}

 	if ( (jmfSecurity != null) && (jmfSecurity.getName().startsWith("jdk12"))) {

	    try {
  		Constructor cons = CreateWorkThreadAction.cons;
		renderThread = (RenderThread) jdk12.doPrivM.invoke(
                                           jdk12.ac,
 					  new Object[] {
 					  cons.newInstance(
 					   new Object[] {
                                               RenderThread.class,
					       BasicRendererModule.class,
					       this
                                           })});
	    } catch (Exception e) {
	    }
 	} else {
	    renderThread = new RenderThread(this);
 	}

	engine = (PlaybackEngine)getController();

	return true;
    }

    public boolean doPrefetch() {
	super.doPrefetch();
	if (!opened) {
	    try {
		renderer.open();
	    } catch (ResourceUnavailableException e) {
		prefetchFailed = true;
		return false;
	    }
	    prefetchFailed = false;
	    opened = true;

	}

	if (! ((PlaybackEngine)controller).prefetchEnabled)
	    return true;

	prefetching = true;

	// We also need to start the render thread.  Otherwise, it won't
	// get the first prefetch frame. 
	renderThread.start();

	return true;
    }

    public void doFailedPrefetch() {
	renderThread.pause();
	renderer.close();
	opened = false;
	prefetching = false;
    }

    public void abortPrefetch() {
	renderThread.pause();
	renderer.close();
	prefetching = false;
	opened = false;
    }

    public void doStart() {
	super.doStart();
	// Clocked renderer is handled by the super.doStart().
	if (!(renderer instanceof Clock))
	    renderer.start();
	prerolling = false;
	started = true;

	synchronized (prefetchSync) {
	    prefetching = false;
	    renderThread.start();
	}
    }

    public void doStop() {
	started = false;
	prefetching = true;
	super.doStop();
	// Clocked renderer is handled by the super.doStop().
	if (renderer != null && !(renderer instanceof Clock))
	    renderer.stop();
    }

    public void doDealloc() {
	renderer.close();
    }

    public void doClose() {
	renderThread.kill();
	if (renderer != null)
	    renderer.close();
	if (rtpTimeBase != null) {
	    RTPTimeBase.remove(this, rtpCNAME);
	    rtpTimeBase = null;
	}
    }

    public void reset() {
	super.reset();
	prefetching = false;
    }

    public void triggerReset() {
	if (renderer != null)
	    renderer.reset();

	synchronized (prefetchSync) {
	    prefetching = false;
	    // If we are already done with the reset, there's no need
	    // to re-start the renderThread.  It's needed only if the
	    // data is blocked at the renderer when reset was called.
	    if (resetted)
		renderThread.start();
	}
    }

    public void doneReset() {
	renderThread.pause();
    }

    /**
     * Attempt to re-initialize the renderer given a new input format.
     */
    protected boolean reinitRenderer(Format input) {
	if (renderer != null) {
	    if (renderer.setInputFormat(input) != null) {
		// Fine, the existing renderer still works.
		return true;
	    }
	}

	if (started) {
	    renderer.stop();
	    renderer.reset();
	}

	renderer.close();
	renderer = null;

	Renderer r;
	if ((r = SimpleGraphBuilder.findRenderer(input)) == null)
	    return false;

	setRenderer(r);
	if (started)
	    renderer.start();

	chunkSize = computeChunkSize(input);

	return true;
    }

    protected void setRenderer(Renderer r) {
	renderer = r;
	if (renderer instanceof Clock)
	    setClock((Clock)renderer);
    }

    public Renderer getRenderer() {
	return renderer;
    }

    // This is triggered from a connectorPushed.
    // Since this module is running the the safe protocol, so it's not
    // applicable.
    protected void process() {
    }

    private long LEEWAY = 10;  //10 msec.
    private long lastRendered = 0;
    private boolean failed = false;
    private boolean notToDropNext = false;
    private Buffer storedBuffer = null;	// the previous buffer when data is
					// not fully consumed.
    private boolean checkRTP = false;
    private boolean noSync = false;

    /**
     * The loop to process the data.
     * It handles the getting and putting back of the data buffers.
     * It in turn calls scheduleBuffer(Buffer) to do the bulk of processing.
     */
    protected boolean doProcess() {

	// Notify the engine if stop time has been reached.
	if ((started || prefetching) && 
	    stopTime > -1 && elapseTime.value >= stopTime) {
	    if (renderer instanceof Drainable)
		((Drainable)renderer).drain();
	    doStop();
	    if (moduleListener != null)
		moduleListener.stopAtTime(this);
	}

	Buffer buffer;

	if (storedBuffer != null)
	    buffer = storedBuffer;
	else {
	    buffer = ic.getValidBuffer();
	    /*
	    System.err.println("TS: " + buffer.getTimeStamp() + 
		" dur: " + buffer.getDuration() + 
		" len: " + buffer.getLength() + 
		" seq: " + buffer.getSequenceNumber() + 
		" eom: " + buffer.isEOM() +
		" discard: " + buffer.isDiscard());
	     */
	}

	if (!checkRTP) {

	    // If this is playing back from RTP, we'll get an RTPTimeBase.
	    // This test cannot be performed at realize time since the
	    // actual rtp DataSource may not be available at that time
	    // for RTSP playback.

	    if ((buffer.getFlags() & Buffer.FLAG_RTP_TIME) != 0) {

		String key = engine.getCNAME();
		if (key != null) {
		    rtpTimeBase = RTPTimeBase.find(this, key);
		    rtpCNAME = key;
		    // Set this as the master if it is playing audio.
		    if (ic.getFormat() instanceof AudioFormat) {
			Log.comment("RTP master time set: " + renderer + "\n");
			//System.err.println("RTP sync kicks in");
			rtpTimeBase.setMaster(this);
		    }
		    checkRTP = true;
		    noSync = false;
		} else {
		    // There's no cname association yet.  We can't do any
		    // synchronization at this point.
		    // We'll have to keep checking back again.
		    noSync = true;
		}
	    } else
		checkRTP = true;
	}

	lastTimeStamp = buffer.getTimeStamp();

	// Check if we are in the resetted state.
	if (failed || resetted) {

	    // Check if the input buffer contains the zero-length
	    // flush flag.  If so, we are almost done.
	    if ((buffer.getFlags() & Buffer.FLAG_FLUSH) != 0) {
		resetted = false;
		renderThread.pause();

		// Notify the engine if the module has done processing.
		if (moduleListener != null)
		    moduleListener.resetted(this);
	    }

	    // In the resetted state, we won't process any of the
	    // data.  We'll just return the buffers unprocessed.
	    storedBuffer = null;
	    ic.readReport();
	    return true;
	}

	if (PlaybackEngine.DEBUG) jmd.moduleIn(this, 0, buffer, true);

	// Schedule the buffer to be rendered.
	boolean rtn = scheduleBuffer(buffer);

	// Handle EOM
	// Also need to make sure this buffer is fully consumed, i.e., 
	// the storedBuffer is not set.
	if (storedBuffer == null && buffer.isEOM()) {

	    if (prefetching)
		donePrefetch();

	    // Eventhough we've received EOM, we haven't finished
	    // processing yet.  We'll need to sleep till its PT.
	    // Otherwise, for low FR movies, the last frame will
	    // not be presented to its full duration.
	    if ((buffer.getFlags() & Buffer.FLAG_NO_WAIT) == 0 &&
		 buffer.getTimeStamp() > 0 && buffer.getDuration() > 0 &&
		 buffer.getFormat() != null &&
		 !(buffer.getFormat() instanceof AudioFormat) &&
		 !noSync) {
		waitForPT(buffer.getTimeStamp() + lastDuration);
	    }

	    storedBuffer = null;
	    ic.readReport();

	    if (PlaybackEngine.DEBUG) jmd.moduleIn(this, 0, buffer, false);
	    doStop();
	    if (moduleListener != null)
		moduleListener.mediaEnded(this);

	    return true;
	}

	// If we are fully done with this buffer, return it.
	if (storedBuffer == null)
	    ic.readReport();

	if (PlaybackEngine.DEBUG) jmd.moduleIn(this, 0, buffer, false);

	return rtn;
    }


    /**
     * Handed a buffer, this function does the scheduling of the
     * buffer processing.  It in turn calls processBuffer to do
     * the real processing.
     */
    protected boolean scheduleBuffer(Buffer buf) {

	int rc = PlugIn.BUFFER_PROCESSED_OK;

	Format format = buf.getFormat();

	if (format == null) {
	    // Something's weird, we'll just assume it's the previous
	    // format.
	    format = ic.getFormat();
	    buf.setFormat(format);
	}

	// Handle mid-stream format change.
	if (format != ic.getFormat() && !format.equals(ic.getFormat()) &&
	    !buf.isDiscard()) {
	    // Return if failed.
	    if (!handleFormatChange(format))
		return false;
	}

	// Signal the engine if the marker bit is set.
	if ((buf.getFlags() & Buffer.FLAG_SYSTEM_MARKER) != 0 &&
	    moduleListener != null) {
	    moduleListener.markedDataArrived(this, buf);
	    buf.setFlags(buf.getFlags() & ~Buffer.FLAG_SYSTEM_MARKER);
	}

	// Now on to scheduling!
	// Whether to do synchronization or not depends on the following
	// predicate.

	if (prefetching || 
	    (format instanceof AudioFormat) || 
	    buf.getTimeStamp() <= 0 ||
	    (buf.getFlags() & Buffer.FLAG_NO_SYNC) == Buffer.FLAG_NO_SYNC ||
	    noSync) {

	    // Handle non-scheduled data.
	    // Audio is handled here too since the data itself dictates
	    // the timing, not the time stamps.
	    // It also handles the prefetching cycle since there's no
	    // need to wait for presentation time.
	   /*
	    if (format instanceof javax.media.format.VideoFormat) {
		System.err.println("BRM: display on prefetch: " + 
					buf.getSequenceNumber());
	    }
	   */

	    if (!buf.isDiscard())
		rc = processBuffer(buf);

	} else  {

	    // Handle scheduled data.
	    // Video with a preset presentation timestamp is handled here.

	    long mt = getSyncTime(buf.getTimeStamp());

	    // Let's bring the #'s back to milli seconds range.
	    long lateBy = mt/1000000L - buf.getTimeStamp()/1000000L - 
				getLatency()/1000000L;

	    /*
	    System.err.println("VR: PT = " + buf.getTimeStamp()/1000000L + 
				" MT = " + mt/1000000L + 
				" lateBy = " + lateBy);
	    */

	    // Check the presentation schedule.

	    if (storedBuffer == null && lateBy > 0) {

		// It's behind schedule.
		//System.err.println("frame behind by: " + lateBy);

		if (buf.isDiscard()) {

		    // The upstream is telling me to discard this frame.
		    // This means that the upstream has drop a frame.
		    // So when the next frame comes, I'll remember not
		    // to drop a frame again.  Otherwise, we'll end up
		    // double-dropping frames.
		    notToDropNext = true;
		    //System.err.println("discard frame");

		} else {

		    if (buf.isEOM()) {

			// Don't drop the next (first frame).
			notToDropNext = true;

		    } else {

			// Report the frame behind time to the engine.
			if (moduleListener != null && 
			    format instanceof VideoFormat) {
			    float fb = lateBy * frameRate / 1000f;
			    if (fb < 1f)
				fb = 1f;
			    moduleListener.framesBehind(this, fb, ic);
			    framesWereBehind = true;
			    //System.err.println("frames behind = " + fb);
			}
		    }

		    if ((buf.getFlags() & Buffer.FLAG_NO_DROP) != 0) {

			// Process the frame if we are not allowed to drop 
			// the frame.

			rc = processBuffer(buf);

		    } else {

			// Do not give up too easily.  Allow for a few more
			// provisions before giving up on rendering the frame.

			if (lateBy < LEEWAY || notToDropNext ||
			    (buf.getTimeStamp() - lastRendered) > 1000000000L) {

			    rc = processBuffer(buf);
			    lastRendered = buf.getTimeStamp();
			    notToDropNext = false;
			} else {
			    //System.err.println("frame dropped");
			}
		    }
		}

	    } else {

		// It's either on time or ahead of schedule.

	        //System.err.println("VR: PT = " + 
		//	buf.getTimeStamp() + " MT = " + mt);

		//System.err.println("frame ahead by: " + lateBy);

		// Report "on time" to the engine.
		if (moduleListener != null && framesWereBehind &&
		    format instanceof VideoFormat) {
		    moduleListener.framesBehind(this, 0f, ic);
		    framesWereBehind = false;
		    //System.err.println("frames behind = 0");
		}

		if (!buf.isDiscard()) {
		    // Wait if we are ahead of the presentation time
		    // or the NO_WAIT flag is off.
		    if ((buf.getFlags() & Buffer.FLAG_NO_WAIT) == 0)
		        waitForPT(buf.getTimeStamp());
	     
		    if (!resetted) {
		        rc = processBuffer(buf);
			lastRendered = buf.getTimeStamp();
		    }
		}
	    }
	}

	// Check for processing return code.

	if ((rc & PlugIn.BUFFER_PROCESSED_FAILED) != 0) {
	    storedBuffer = null;
	} else if ((rc & PlugIn.INPUT_BUFFER_NOT_CONSUMED) != 0) {
	    // Save what's left for the next processing round.
	    // Do not return the buf back.
	    storedBuffer = buf;
	} else {
	    // Success.
	    storedBuffer = null;
	    if (buf.getDuration() >= 0)
		lastDuration = buf.getDuration();
	}

	return true;
    }


    final float MAX_RATE = 1.05f;
    final float RATE_INCR = .01f;
    final int FLOW_LIMIT = 20;
    boolean overMsg = false;
    int overflown = FLOW_LIMIT / 2;
    float rate = 1.0f;

    /**
     * Break down one larger buffer into smaller pieces so the
     * processing won't take that long to block.
     */
    public int processBuffer(Buffer buffer) {
	int remain = buffer.getLength();
	int offset = buffer.getOffset();
	int len, rc = PlugIn.BUFFER_PROCESSED_OK;
	boolean isEOM = false;

	// Data flow management.  If the FLAG_BUF_OVERFLOWN flag
	// is set, we'll try to speed up the renderer to catch up.
	// This is beneficial for streaming media when the server
	// clock is faster than the client clock.
	if (renderer instanceof Clock) {
	    if ((buffer.getFlags() & buffer.FLAG_BUF_OVERFLOWN) != 0)
		overflown++;
	    else
		overflown--;

	    if (overflown > FLOW_LIMIT) {

		if (rate < MAX_RATE) {
		    rate += RATE_INCR;
		    renderer.stop();
		    ((Clock)renderer).setRate(rate);
		    renderer.start();
		    if (!overMsg) {
			Log.comment("Data buffers overflown.  Adjust rendering speed up to 5 % to compensate"); 
			overMsg = true;
		    }
		}

		overflown = FLOW_LIMIT / 2;

	    } else if (overflown <= 0) {

		if (rate > 1.0f) {
		    rate -= RATE_INCR;
		    renderer.stop();
		    ((Clock)renderer).setRate(rate);
		    renderer.start();
		}

		overflown = FLOW_LIMIT / 2;
	    }
	}

	// Each buffer is broken down into smaller chunks for processing
	// as defined by chunkSize.
	//
	// EOM is trickier.  We don't want to send multiple EOM's to
	// the renderer for each of the smaller chunks.  So we catch
	// it and send it only on the last chunk.
	do {

	    // Check for the preset stop time.  Return if we are done.
	    if (stopTime > -1 && elapseTime.value >= stopTime) {
		if (prefetching)
		    donePrefetch();
		return PlugIn.INPUT_BUFFER_NOT_CONSUMED;
	    }

	    // If we are prerolling, there's no need to break the data
	    // into smaller chunks for processing.
	    if (remain <= chunkSize || prerolling) {
		if (isEOM) {
		    isEOM = false;
		    buffer.setEOM(true);
		}
		len = remain;
	    } else {
		if (buffer.isEOM()) {
		    isEOM = true;
		    buffer.setEOM(false);
		}
		len = chunkSize;
	    }

	    buffer.setLength(len);
	    buffer.setOffset(offset);

	    if (prerolling && !handlePreroll(buffer)) {
		offset += len;
		remain -= len;
		continue;
	    }

	    try {

		rc = renderer.process(buffer);

	    } catch (Throwable e) {
		Log.dumpStack(e);
		if (moduleListener != null)
		    moduleListener.internalErrorOccurred(this);
	    }

	    if ((rc & PlugIn.PLUGIN_TERMINATED) != 0) {
		failed = true;
		if (moduleListener != null)
		    moduleListener.pluginTerminated(this);
		return rc;
	    }

	    if ((rc & PlugIn.BUFFER_PROCESSED_FAILED) != 0) {
		buffer.setDiscard(true);
		if (prefetching)
		    donePrefetch();
		return rc;

	    } 

	    if ((rc & PlugIn.INPUT_BUFFER_NOT_CONSUMED) != 0) {
		// Check what's been processed so far.
		 len -= buffer.getLength();
	    }

	    offset += len;
	    remain -= len;

	    // If the module is prefetching, we'll need to check to see 
	    // if the device has been prefetched.
	    if (prefetching && 
		(!(renderer instanceof Prefetchable) || 
		 ((Prefetchable)renderer).isPrefetched())) {
		// If EOM happens prefetch, disable the EOM.
		// We'll get another EOM from the source module again.
		isEOM = false;
		buffer.setEOM(false);
		donePrefetch();
		break;
	    }

	    elapseTime.update(len, buffer.getTimeStamp(), buffer.getFormat());

	} while (remain > 0 && !resetted);

	// Re-enable the EOM flag if it were disabled previously.
	if (isEOM)
	    buffer.setEOM(true);

	buffer.setLength(remain);
	buffer.setOffset(offset);

	if (rc == PlugIn.BUFFER_PROCESSED_OK)
	    framesPlayed++;

	return rc;
    }


    /**
     * Handles mid-stream format change.
     */
    private boolean handleFormatChange(Format format) {
	// The format is changed mid-stream!
	if (!reinitRenderer(format)) {
	    // Failed.
	    storedBuffer = null;
	    failed = true;
	    if (moduleListener != null)
		moduleListener.formatChangedFailure(this, ic.getFormat(), format);
	    return false;
	}

	Format oldFormat = ic.getFormat();
	ic.setFormat(format);
	if (moduleListener != null)
	    moduleListener.formatChanged(this, oldFormat, format);

	if (format instanceof VideoFormat) {
	    float fr = ((VideoFormat)format).getFrameRate();
	    if (fr != VideoFormat.NOT_SPECIFIED)
		frameRate = fr;
	}

	return true;
    }


    /**
     * Handles the aftermath of prefetching.
     */
    private void donePrefetch() {

	synchronized (prefetchSync) {
	    if (!started && prefetching)
		renderThread.pause();
	    prefetching = false;
	}

	if (moduleListener != null)
	    moduleListener.bufferPrefetched(this);
    }


    /**
     * Enable prerolling.
     */
    public void setPreroll(long wanted, long actual) {
	super.setPreroll(wanted, actual);
	elapseTime.setValue(actual);
    }


    /**
     * Handle the prerolling a buffer.  It will preroll until the media
     * has reach the current media time before displaying.
     */
    protected boolean handlePreroll(Buffer buf) {

	if (buf.getFormat() instanceof AudioFormat) {

	    if (!hasReachAudioPrerollTarget(buf))
		return false;

	} else if ((buf.getFlags() & Buffer.FLAG_NO_SYNC) != 0 || 
	    buf.getTimeStamp() < 0) {

	    // The data is non-time specific.
	    // Deliberately empty at this point.

	} else if (buf.getTimeStamp() < getSyncTime(buf.getTimeStamp())) {
	    // The data is time-specific and it hasn't yet reached the
	    // target media time.  So we are skipping it.

	    //System.err.println("preroll video: " + buf.getTimeStamp());
	    return false;
	}

	/*
	if (buf.getFormat() instanceof AudioFormat)
	    System.err.println("done prerolling audio: " + buf.getLength());
	else
	    System.err.println("done prerolling video: " + buf.getLength());
	*/

	// The preroll target has been reached.
	prerolling = false;

	return true;
    }


    /**
     * Return true if given the input buffer, the audio will reach the
     * target preroll time -- the current media time.
     */
    private boolean hasReachAudioPrerollTarget(Buffer buf) {

	//System.err.println("preroll audio: " + buf.getLength());
	long target = getSyncTime(buf.getTimeStamp());

	elapseTime.update(buf.getLength(), buf.getTimeStamp(), buf.getFormat());

	if (elapseTime.value >= target) {
	    long remain = ElapseTime.audioTimeToLen(
					elapseTime.value - target,
					(AudioFormat)buf.getFormat());
	    int offset = buf.getOffset() + buf.getLength() - (int)remain;
	    if (offset >= 0) {
		buf.setOffset(offset);
		buf.setLength((int)remain);
	    }

	    elapseTime.setValue(target);

	    return true;
	}

	return false;
    }


    // Used in waitForPT.  It computes dynamically the error inherent
    // in System.sleep().
    long systemErr = 0L;

    /**
     * If the presentation time has not been reached, this function
     * will wait until that happens.
     */
    private boolean waitForPT(long pt) {
	long mt = getSyncTime(pt);
	long aheadBy, lastAheadBy = -1;
	long interval;
	long before, slept;
	int beenHere = 0;

	aheadBy = (pt - mt)/1000000L;
	if (rate != 1.0f)
	    aheadBy = (long)((float)aheadBy / rate);

	while (aheadBy > systemErr && !resetted) {

	    if (aheadBy == lastAheadBy) {
		// Somehow, time hasn't changed at all since the last
		// time we slept (perhaps no audio samples updated),
		// we'll use a different scheme to compute the interval.

		// We'll compute the regular interval, plus an additional
		// 3 msecs every time we are here until we reach 33 msecs.
		interval = aheadBy + (5 * beenHere);
		if (interval > 33L)
		    interval = 33L;
		else
		    beenHere++;
		//System.err.println("been here: " + beenHere);
	    } else {
		interval = aheadBy;
		beenHere = 0;
	    }

	    // Don't sleep more than 1/8 sec.
	    // We'll wake up and check time again.
	    interval = (interval > 125L ? 125L : interval);

	    //System.err.println("mt = " + mt + " pt = " + pt);
	    //System.err.println("interval = " + interval);

	    before = System.currentTimeMillis();

	    // The interval is scheduled at ahead of time by the
	    // expected system error.
	    interval -= systemErr;

	    try {
		if (interval > 0)
		    Thread.currentThread().sleep(interval);
	    } catch (InterruptedException e) {}

	    slept = System.currentTimeMillis() - before;

	    // Compute the system err: the actual time slept minus the
	    // the desired sleep time.  Then take the average.
	    systemErr = (slept - interval + systemErr)/2;

	    // Rule out some illegal numbers.
	    if (systemErr < 0)
		systemErr = 0;
	    else if (systemErr > interval)
		systemErr = interval;

	    //System.err.println("slept = " + slept + " err = " + systemErr);

	    // Check the time again to see if we need to sleep more.
	    mt = getSyncTime(pt);

	    lastAheadBy = aheadBy;
    	    aheadBy = (pt - mt)/1000000L;
	    if (rate != 1.0f)
		aheadBy = (long)((float)aheadBy / rate);

	    if (getState() != Controller.Started)
		break;
	}
	return true;
    }

    // Set a limit here.  Cannot sync beyond a 2 sec difference.
    static final long RTP_TIME_MARGIN = 2000000000L;
    boolean rtpErrMsg = false;

    private long getSyncTime(long pts) {
	if (rtpTimeBase != null) {
	    // If we are the master, we don't need to request time
	    // from the rtpTimeBase.
	    if (rtpTimeBase.getMaster() == getController())
		return pts;
	    long ts = rtpTimeBase.getNanoseconds();
	    // Cannot sync beyond a limit.
	    if (ts > pts + RTP_TIME_MARGIN || ts < pts - RTP_TIME_MARGIN) {
		/*
		System.err.println("pts and mts too different: ts = " + 
			ts/1000000L + " pts = " + 
			pts/1000000L + " diff = " + 
			(ts - pts)/1000000L);
		*/
		if (!rtpErrMsg) {
		    Log.comment("Cannot perform RTP sync beyond a difference of: " + (ts - pts)/1000000L + " msecs.\n");
		    rtpErrMsg = true;
		}
		//System.err.println("No RTP Sync: " + (ts - pts)/1000000L + " msecs.\n");
		return pts;
	    } else
		return ts;
	} else
	    return getMediaNanoseconds();
    }

    long lastTimeStamp;
    public long getRTPTime() {
	if (ic.getFormat() instanceof AudioFormat) {
	   if (renderer instanceof AudioRenderer) {
		/*
		  System.err.println("rtpTime[audio]: " + 
			lastTimeStamp + " latency: " 
			+ ((AudioRenderer)renderer).getLatency() + " TS: " +
			    (lastTimeStamp
				- ((AudioRenderer)renderer).getLatency()));
		 */
		return lastTimeStamp - ((AudioRenderer)renderer).getLatency();
	   } else {
		//System.err.println("rtpTime[audio]: " + lastTimeStamp);
		return lastTimeStamp;
	   }
	} else {
	   //System.err.println("rtpTime[video]: " + lastTimeStamp);
	   return lastTimeStamp;
	}
    }

    public Object[] getControls() {
	return renderer.getControls();
    }

    public Object getControl(String s) {
	return renderer.getControl(s);
    }

    public void setFormat(Connector connector, Format format) {
	renderer.setInputFormat(format);
	if (format instanceof VideoFormat) {
	    float fr = ((VideoFormat)format).getFrameRate();
	    if (fr != VideoFormat.NOT_SPECIFIED)
		frameRate = fr;
	}
    }

    public int getFramesPlayed() {
	return framesPlayed;
    }

    public void resetFramesPlayed() {
	framesPlayed = 0;
    }

    static final int MAX_CHUNK_SIZE = 16;	// 1/16 of a sec.
    AudioFormat ulawFormat = new AudioFormat(AudioFormat.ULAW);
    AudioFormat linearFormat = new AudioFormat(AudioFormat.LINEAR);
    private int computeChunkSize(Format format) {

	// Break up the data if it's linear or ulaw audio.
	if (format instanceof AudioFormat && 
	    (ulawFormat.matches(format) || linearFormat.matches(format))) {

	    AudioFormat af = (AudioFormat)format;
	    int units = af.getSampleSizeInBits() * af.getChannels() / 8;
	    if (units == 0) // sample size < 1 byte.
		units = 1;
	    int chunks = (int)af.getSampleRate() * units / MAX_CHUNK_SIZE;

	    // Chunks should be in multiples of the independent units.
	    return (int)(chunks / units * units);
	}

	return Integer.MAX_VALUE;
    }
}

////////////////////////////////
//
// Inner classes not! $$
////////////////////////////////

class RenderThread extends LoopThread {
    
    BasicRendererModule module;
    //    public RenderThread() {
    public RenderThread(BasicRendererModule module) {
	this.module = module;
 	setName(getName() + ": " + module.renderer);
	useVideoPriority();
    }
    
    protected boolean process() {
	return module.doProcess();
    }
}


