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

package com.sun.media.multiplexer;

import com.sun.media.*;
import javax.media.*;
import javax.media.protocol.*;
import javax.media.format.*;
import javax.media.format.AudioFormat;
import java.io.IOException;
import com.sun.media.controls.MonitorAdapter;
import com.sun.media.util.MediaThread;
import com.sun.media.util.ElapseTime;

public class RawSyncBufferMux extends RawBufferMux {

    // used for MPEG video synchronization.
    boolean mpegBFrame = false;
    boolean mpegPFrame = false;

    // If this flag is on, the timestamp of the buffers will be
    // monotonically increasing.  This is used for RTP in particular.
    protected boolean monoIncrTime = false;
    private long monoStartTime = 0;	// in nanoseconds.
    private long monoTime = 0;

    private Object waitLock = new Object();
    private boolean resetted = false;
    private boolean masterTrackEnded = false;

    // For comparing formats.
    static AudioFormat mpegAudio = new AudioFormat(AudioFormat.MPEG_RTP);
    static VideoFormat mpegVideo = new VideoFormat(VideoFormat.MPEG_RTP);

    // Constructor 
    public RawSyncBufferMux(){
	super();
	timeBase = new RawMuxTimeBase();
	// Allow data to be dropped.
	allowDrop = true;
	clock = new BasicClock();
	try{
	    clock.setTimeBase(timeBase);
	} catch (Exception e){ }
    }

    public boolean initializeTracks(Format[] trackFormats){

	if (!super.initializeTracks(trackFormats))
	    return false;

	masterTrackID = 0;
	for (int i = 0; i < trackFormats.length; i++) {
	    if (trackFormats[i] instanceof AudioFormat)
		masterTrackID = i;
	}

	return true;
    }

    public void reset(){
	super.reset();
	mpegBFrame = false;
	mpegPFrame = false;
	synchronized (waitLock) {
	    resetted = true;
	    waitLock.notify();
	}
    }

    /**
     * Returns a descriptive name for the plug-in.
     * This is a user readable string.
     */
    public String getName(){
	return "Raw Sync Buffer Multiplexer";
    }

    /**
     * Process the buffer and multiplex it with data from other
     * tracks.  The multiplexed output is sent to the output 
     * <code>DataSource</code>. 
     * @param buffer the input buffer
     * @param trackID the index identifying the track where the input buffer
     *          belongs.
     * @return BUFFER_PROCESSED_OK if the processing is successful.  Other
     * possible return codes are defined in PlugIn. 
     * @see PlugIn
     */
    public int process(Buffer buffer, int trackID) {

	// If the processor starts out having RTP times, before the
	// data comes out of this processor, we should reset the
	// RTP flag and sets it to RELATIVE time.  Otherwise, the
	// next guy in the processing chain may compute the time
	// incorrectly.
	if ((buffer.getFlags() & Buffer.FLAG_RTP_TIME) != 0) {
	    buffer.setFlags((buffer.getFlags() & ~Buffer.FLAG_RTP_TIME) |
				Buffer.FLAG_RELATIVE_TIME);
	}

	// If the monitor is enabled, we'll send the data to the monitor.
	if (mc[trackID] != null && mc[trackID].isEnabled())
	    mc[trackID].process(buffer);

	if ((streams == null) || (buffer == null) || (trackID >= streams.length)){
	    return PlugIn.BUFFER_PROCESSED_FAILED;
	}

	if (buffer.isDiscard())
	    return BUFFER_PROCESSED_OK;

	//
	// Unless the NO_WAIT flag is on, we'll need to wait for
	// the presentation time.
	//
	if ((buffer.getFlags() & Buffer.FLAG_NO_WAIT) == 0) {

	    if (buffer.getFormat() instanceof AudioFormat) {
		// Regular audio requires that we wait for the last
		// bit of audio to be done.  But MPEG's timestamp is
		// at the beginning of the chunk.
		if (mpegAudio.matches(buffer.getFormat()))
		    waitForPT(buffer.getTimeStamp(), trackID);
		else
		    waitForPT(mediaTime[trackID], trackID);
	    } else if (buffer.getTimeStamp() >= 0) {
		if (mpegVideo.matches(buffer.getFormat()) &&
			(buffer.getFlags() & Buffer.FLAG_RTP_MARKER) != 0) {
		    byte[] payload = (byte[])buffer.getData();
		    int offset = buffer.getOffset();
		    int ptype = payload[offset+2] & 0x07;
		    if (ptype > 2) {
			// found a B frame
			mpegBFrame = true;
		    } else if (ptype == 2) {
			// found a P frame
			mpegPFrame = true;
		    }

		    /*
		     * For MPEG the timestamp is the time of the frame but
		     * frames come out of order. Since a stream may not
		     * have all types of frames, have to allow for all
		     * mixes. If B frames are present, only wait on them.
		     * If no B frames, wait on P frames. If no B or P frames
		     * wait on I frames.
		     */
		    if (ptype > 2 || (ptype == 2 && !mpegBFrame)
				|| (ptype == 1 && !(mpegBFrame | mpegPFrame))) {
			waitForPT(buffer.getTimeStamp(), trackID);
		    }
		} else {
		    waitForPT(buffer.getTimeStamp(), trackID);
		}
	    }
	}

	updateTime(buffer, trackID);
	    
	// We are doing the synchronization here so the down stream
	// will not need to.
	buffer.setFlags(buffer.getFlags() | Buffer.FLAG_NO_SYNC);
	
	if (!(buffer.getFormat() instanceof AudioFormat) ||
	     mpegAudio.matches(buffer.getFormat())) {
	    // Convert the timestamps to be monotically increasing.
	    if (monoIncrTime) {
		monoTime = monoStartTime +
			buffer.getTimeStamp() - mediaStartTime * 1000000;

	     /*
		if ((buffer.getFlags() & Buffer.FLAG_RTP_MARKER) != 0)
	    	    System.err.println("monoStartTime = " + monoStartTime +
			" mediaStartTime = " + mediaStartTime +
			" TS = " + buffer.getTimeStamp() +
			" mono TS = " + monoTime);
	      */
		buffer.setTimeStamp(monoTime);
	    }
	}

	if (buffer.isEOM() && trackID == masterTrackID)
	    masterTrackEnded = true;

	buffer.setHeader(new Long(System.currentTimeMillis()));

	return streams[trackID].process(buffer);
    }

    public void syncStart(Time at) {
	masterTrackEnded = false;
	super.syncStart(at);
    }

    public void setMediaTime(Time now) {
	super.setMediaTime(now);
	monoStartTime = monoTime + 10;	// This is so the next frame time
					// will not be exactly the same.
    }

    /**
     * Update the media time per track.
     */
    protected void updateTime(Buffer buf, int trackID) {

	if (buf.getFormat() instanceof AudioFormat) {

	    if (mpegAudio.matches(buf.getFormat())) {
		if (buf.getTimeStamp() < 0) {
		    if (systemStartTime >= 0)
			mediaTime[trackID] = (mediaStartTime + 
			    System.currentTimeMillis() - systemStartTime) * 1000000;
		} else
		    mediaTime[trackID] = buf.getTimeStamp();
	    } else {
		long t = ((AudioFormat)buf.getFormat()).computeDuration(buf.getLength());

		if (t >= 0)
		    mediaTime[trackID] += t;
		else
		    mediaTime[trackID] = buf.getTimeStamp();
	    }

	} else {
	    if (buf.getTimeStamp() < 0 && systemStartTime >= 0)
		mediaTime[trackID] = (mediaStartTime + 
			  System.currentTimeMillis() - systemStartTime) * 1000000;
	    else
		mediaTime[trackID] = buf.getTimeStamp();
	}

	//System.err.println("mediaTime = " + mediaTime[trackID]);

	timeBase.update();
    }


    static int THRESHOLD = 80;
    static int LEEWAY = 5;
 
    private void waitForPT(long pt, int trackID) {
        long delay;

	pt = pt / 1000000;	// To bring it to millisecs range.

	//System.err.println("MT = " + mediaStartTime + 
	//		" ST = " + systemStartTime + 
	//		" pt = " + pt + 
	//		" st = " + System.currentTimeMillis());

	if (masterTrackID == -1 || trackID == masterTrackID) {
	    if (systemStartTime < 0)
		delay = 0;
	    else
		delay = (pt-mediaStartTime) - 
			(System.currentTimeMillis()-systemStartTime); 
	} else {
	    delay = pt - mediaTime[masterTrackID]/1000000;
	}

	//System.err.println("delay = " + delay);

	// This is a workaround for now.  The video capture pipeline
	// is not fully flushed causing wrong values in the timestamps.
	if (delay > 2000)
	    return;

        while (delay > LEEWAY && !masterTrackEnded) {
            if (delay > THRESHOLD)
                delay = THRESHOLD;

	    synchronized (waitLock) {
		try {
		    waitLock.wait(delay);
		} catch (Exception e) {
		    break;
		}
		if (resetted) {
		    resetted = false;
		    break;
		}
	    }

	    if (masterTrackID == -1 || trackID == masterTrackID)
		delay = (pt-mediaStartTime) - 
			(System.currentTimeMillis()-systemStartTime); 
	    else
		delay = pt - mediaTime[masterTrackID]/1000000;
        }
    }

}// end of class RawBufferMultiplexer
 

