/*
  #  $Id$
  # -----------------------------------------------------------------------------
  #  The part of 'MediaPlayer' project
  # -----------------------------------------------------------------------------
  #  Author: Petrov Maxim, 09/01/2005
  #  Edited by: Andriy Baranetsky
  #  QA by:
  #  Copyright: videoNEXT LLC
  # -----------------------------------------------------------------------------
 */

package com.videonext.mplayer.internal;

import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.*;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.*;
import java.util.Date;
import java.util.Hashtable;
import java.lang.Math;

import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.swing.JPanel;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MediaPlayer extends JPanel implements ComponentListener
{
	private static final long serialVersionUID = 6546613028913051662L;

	public static final int DEFAULT_MAX_HARDWARE_FRAME_RATE = 120;

	public static final int STATUS_INITIALIZED   = -1;
	public static final int STATUS_IDLE          = 0;
	public static final int STATUS_PLAY_SERVER   = 1;
	public static final int STATUS_PLAY_BUFFER   = 2;
	public static final int STATUS_STOPPED       = 3;
	public static final int STATUS_OPENING       = 4;
	public static final int STATUS_BUFFERING     = 5;
	public static final int STATUS_OPENED        = 6;	
	

	public static final int SIGN_NONE = 0;
	public static final int SIGN_RED_CROSS = 1;

	private Logger log;
	private String playerId;
	private boolean isWorking = false;
	private ObjectsTracker objsTracker = null;

	// image
	private int width = 0;	// image width
	private int height = 0;	// image height
	private boolean displayBusy = false;
	private BufferedImage im = null;	
	private BufferedImage backBufferImage = null;
	private Rectangle screenRect = new Rectangle(0, 0, 1, 1);

	private int streamOverTCP = 1;
	private int streamOverHTTP = 0;
	private int maxHWFrameRate = DEFAULT_MAX_HARDWARE_FRAME_RATE;
	private int cacheSize = 0;	// Size of video/audio cache	

	private int jitterBuffer = 0;
	private int useHWDecoder = 0;

	private String videoURL = null;
	private long mpVideoID = 0;
	private VideoSession videoSession = null;	
	private boolean videoPaused = false;
	private boolean serverVideoPaused = false;

	private String audioURL = null;
	private long mpAudioID = 0;	
	private AudioSession audioSession = null;
	private SourceDataLine audioLine = null;	
	private boolean audioPaused = false;
	private boolean serverAudioPaused = false;

	public int isConnecting = 0;	
	private int direction = 1;		// Direction of play video in buffer. If negative value - playing back. 
	private int stepMode = 0;
	private int status = STATUS_IDLE;
	private boolean mute = true;	

	private boolean isActive = true;
	private boolean isScreenCleared = false; 
	private String errorMessage = null; 
	private int signId = SIGN_NONE;
	private String resultMsg = "";
	private int resultCode = 0;
	private Font messageFont = new Font(Font.DIALOG, Font.BOLD, 14);	

	private Hashtable visibleMetadata = new Hashtable();

	// TA1561:
	private boolean isCenterSignShown = false;
	private AtomicBoolean clearScreenMargins = new AtomicBoolean(true);

	protected float frameRate = 0; // number of frames per second
	protected int nextFrameTimeout = 2000; // timeout between frames in milliseconds
	protected boolean isStretchPixelAspectRatio = false; // stretch aspect ratio. Parameter going from JNI
	protected float pixelAspectRatio = 0f; // auto aspect ratio. Parameter going from JNI

	protected String streamType;
	protected String analyticsWarnMsg = null;

	// frame timestamps
	protected boolean liveMode = false;
	protected long frameTimestamp = 0;
	protected long framePrevTimestamp = 0;
	protected long bufferStartTimestamp = 0;
	protected long bufferEndTimestamp = 0;

	private ChangeStatusEvent changeStatusEvent; 	// status queue
	private ZoomControl zoomControl = new ZoomControl();	// zooming 
	private String snapshotFileName = null;	

	private MediaPlayerListener listener;

	private boolean showSnapshotDialog = true;
	private boolean videoDisplayed = false;
	
	private Lock imageMutex = new ReentrantLock(true);
	
	JPanel overlay =  new JPanel();
	
	
	static {
		// Make sure that awt.dll is loaded before loading jawt.dll. Otherwise
        // a Dialog with "awt.dll not found" might pop up.
        // See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4481947.
        Toolkit.getDefaultToolkit();
        
        // Must pre-load JAWT on all non-Mac platforms to
        // ensure references to JAWT_GetAWT()  will succeed 
        // since JAWT shared object isn't in default library path
		System.loadLibrary("jawt");

		
		String playerVersion = NativeHelper.getPlayerVersion();
		//
		// Loading MediaPlayer library
		//
		if (playerVersion == null) { 
			System.err.println("Player version is not set");
		}

		String nativeLibName = null; 

		String sys = System.getProperty("os.name");
		String arch = System.getProperty("os.arch");
		if (sys == null) {
			System.err.println("Unable to identify your OS");
		}

		if (sys.startsWith("Windows")) {
			if (arch.startsWith("x86"))
				nativeLibName = "VNMediaClient.dll"; 
			else
				nativeLibName = "VNMediaClient-64.dll"; 

		} else if (System.getProperty("mrj.version") != null || sys.startsWith("Mac")) { 
			// Mac OS X TODO: check version
			nativeLibName = "libVNMediaClient.dylib";
		} else {
			// assuming it's linux..
			if (arch.startsWith("x86"))
				nativeLibName = "libVNMediaClient.so";
			else
				nativeLibName = "libVNMediaClient-64.so";
		}


		try {
			NativeHelper.loadFromJar(playerVersion, nativeLibName);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	
	/**
	 * MediaPlayer constructor
	 * @param playerId
	 * @throws Exception 
	 */
	public MediaPlayer(String playerId) throws Exception {
		this(playerId, Logger.getAnonymousLogger());

		setLayout(null);
        overlay.setBounds(0, 0, 1, 1);
        overlay.setOpaque(false);
        this.add(overlay);
        
        
		Handler handler = new ConsoleHandler();
		handler.setLevel(Level.ALL);
		handler.setFormatter(new LogFormatter(playerId));
		log.addHandler(handler);
		log.setLevel(Level.INFO);		
		log.setUseParentHandlers(false);
		
		init();
	}

	/**
	 * MediaPlayer constructor
	 * @param playerId
	 * @param log
	 * @throws Exception 
	 */
	public MediaPlayer(String playerId, Logger log) throws Exception {
		this.playerId = playerId;
		this.log = log;

		init();
	}

	/**
	 * Play media session.
	 * @param url Can be either video or audio
	 * @param mediaType 0-video, 1-audio
	 */
	private native void open(String url, int streamOverTCP, int streamOverHTTP, int mediaType, int maxHWFrameRate);
	private native void play(long mpID);
	private native void stop(long mpID);
	private native void pause(long mpID);
	private native void speed(long mpID, float value);
	private native void stepMode(long mpID, int value);
	private native void jumpToTimestamp(long mpID, int value);
	private native void saveCurrentVideoFrame(String fileName, byte[] data);
	private native void setBandwidth(long mpID, int value);
	private native void setJitterBufferLen(long mpID, int value);
	
	/**
	 * Comma separated list
	 */
	private native void setAnalyticsKeysVisualisation(long mpID, String keyList);

	/**
	 * if direction is negative number - playing back, else playing forward
	 */
	private native void resume(long mpID, int direction);


	public void setCacheSize(int cacheSize) {
		this.cacheSize = cacheSize;
	}

	public void setJitterBuffer(int value) {
		this.jitterBuffer = value;
		
		if (mpVideoID != 0)	{
			this.setJitterBufferLen(mpVideoID, value);
		}
	}

	public void setUseHWDecoder(int value) {
		this.useHWDecoder = value;
	}


	public void setEnableLogging(boolean value) {
		for (Handler h : log.getHandlers()) {
			if (value)
				h.setLevel(Level.ALL);
			else
				h.setLevel(Level.OFF);
		}
	}
	
	public void setMediaPlayerListener(MediaPlayerListener listener) {
		this.listener = listener;
		zoomControl.setMediaPlayerListener(listener);
	}

	private void init() throws Exception
	{
		isScreenCleared = true;
		addComponentListener(this);

		addMouseListener(zoomControl);
		addMouseMotionListener(zoomControl);
		addMouseWheelListener(zoomControl);

		zoomControl.setScreenRect(this.screenRect);
		zoomControl.setCanvas(this);

		isWorking = true;
		changeStatusEvent = new ChangeStatusEvent();
		changeStatusEvent.start();
		log("Player initialized");
	}


	public void setOnHold(boolean isOnHold) {
		if (isOnHold) {
			isActive = false;
		} else {
			isActive = true;
			repaint();
		}
	}


	public void destroy() {
		isWorking = false;

		stopSessions();

		// stop status thread
		changeStatusEvent.done();
		try {
			changeStatusEvent.wait();
		} catch (InterruptedException e) {
		} catch (IllegalMonitorStateException e) {}
		changeStatusEvent = null;

		// release resources
		removeComponentListener(this);

		removeMouseWheelListener(zoomControl);
		removeMouseMotionListener(zoomControl);
		removeMouseListener(zoomControl);

		if (im != null) {
			im.flush();
		}
		messageFont = null;	
	}


	public void setParameter(String param, String value)
	{
		if (!isWorking) {
			log.severe("Setting parameter \"" + param + " = " + value + "\" when player is not ready");
			return;
		}		

		log.info("setParameter: " + param + " = " + value);

		if (param.equals("streamOverTCP")) {
			if (value.equals("1") || value.equals("true")) {
				streamOverTCP = 1;
			} else if (value.equals("0") || value.equals("false")) {
				streamOverTCP = 0;
			}
		} else if (param.equals("streamOverHTTP")) {
			if (value.equals("1") || value.equals("true")) {
				streamOverHTTP = 1;
			} else if (value.equals("0") || value.equals("false")) {
				streamOverHTTP = 0;
			}
		}
		else if (param.equals("videoURL"))	{
			videoURL = value;
			liveMode = ! videoURL.contains("start=");
			frameTimestamp = 0;
			bufferStartTimestamp = 0;
			bufferEndTimestamp = 0;
		}
		else if (param.equals("audioURL"))
		{
			audioURL = value;
			liveMode = ! audioURL.contains("start=");			
			frameTimestamp = 0;
			bufferStartTimestamp = 0;
			bufferEndTimestamp = 0;
		}
		else if (param.equals("direction"))
		{
			if (value.equals("1")) {
				direction = 1;
			} else if (value.equals("-1")) { 
				direction = -1;
			}
		}
		else if (param.equals("streamSpeed")) {
			try {
				Float val = Float.parseFloat(value);
				changeSessionsSpeed(val.floatValue());
			} catch (NumberFormatException e) {
				log.warning("Parameter \"streamSpeed\" must be float value!");
			}  
		}   
		else if (param.equals("stepMode"))	{
			if (value.equals("1") || value.equals("on")) {
				changeStepMode(1);
			} else if (value.equals("0") || value.equals("off")) {
				changeStepMode(0);
			} else {
				log.warning("Parameter \"stepMode\" must be one of [0,1,on, off]");
			}
		}
		else if (param.equals("jump2timestamp")) {
			try {
				Integer val = Integer.parseInt(value);
				jump2timestamp(val.intValue());
			} catch (NumberFormatException e) {
				log.warning("Parameter \"jump2timestamp\" must be integer value!");
			}  
		}             
		else if (param.equals("snapshotFileName")) {
			snapshotFileName = value;   
		}
		else if (param.equals("showSnapshotDialog")) {
			if (value.equals("1") || value.equals("on")) {
				showSnapshotDialog = true;
			} else if (value.equals("0") || value.equals("off")) {
				showSnapshotDialog = false;
			} else {
				log.warning("Parameter \"showSnapshotDialog\" must be one of [0,1,on, off]");
			}
		}
		else if (param.equals("frameRate")) {
			try {
				Float val = Float.parseFloat(value);
				frameRate = Math.abs(val.floatValue());
				nextFrameTimeout = (frameRate == 0.0) ? 2000 : Math.round(1000/frameRate) + 2000;
			} catch (NumberFormatException e) {
				log.warning("Parameter \"frameRate\" must be float value!");
				frameRate = 0;
				nextFrameTimeout = 2000;
			}
		}
		else if (param.equals("pixelAspectRatio"))
		{
			if (value.equals("stretch"))
			{
				this.isStretchPixelAspectRatio = true;
			} else {
				this.isStretchPixelAspectRatio = false;
				try {
					Float val = Float.parseFloat(value);
					this.pixelAspectRatio = Math.abs(val.floatValue());
					zoomControl.setPixelAspectRatio(pixelAspectRatio);
				} catch (NumberFormatException e) {
					//log.warning("[" + playerId + "] \"pixelAspectRatio\" must be float value!");
				}
			}
		}
		else if (param.equals("bandwidth"))	{
			try {
				Integer val;
				if (value.equals("auto")) {
					val = 0;
				} else if (value.equals("full")) {
					val = -1;
				} else	{
					val = Integer.parseInt(value);
				}

				if (mpVideoID != 0)	{
					this.setBandwidth(mpVideoID, val.intValue());                    
				}

				if (mpAudioID != 0)	{
					this.setBandwidth(mpAudioID, val.intValue());
				}
			} catch (NumberFormatException e) {
				log.warning("Parameter \"bandwidth\" must be integer value!");
			}
		} else if (param.equalsIgnoreCase("maxHWFrameRate")) {
			try {
				int val = Integer.parseInt(value);
				if (val <= 1) {
					throw new NumberFormatException();
				} else {
					maxHWFrameRate = val;
				}
			} catch (NumberFormatException e) {
				log.warning("Invalid value of parameter \"" + param + "\"");
			}
		} else if (param.equals("mute")) {
			if (value.equals("1") || value.equals("on")) {
				mute = true;
				if (audioLine != null) {
					audioLine.flush();
				}
			} else if (value.equals("0") || value.equals("off")) {
				mute = false;
			} else {
				log.warning("Parameter \"mute\" must be one of [0,1,on,off]");
			}
		} else if( param.equals("metadataTagInfo") ) {
			if (mpVideoID != 0)	{
				this.setMetadataTagInfo(mpVideoID, value);                    
			}
			if (mpAudioID != 0)	{
				this.setMetadataTagInfo(mpAudioID, value);
			}
		}
		else if (param.equals("analyticsKeysVisualisation")) {
			if (mpVideoID != 0)	{
				this.setAnalyticsKeysVisualisation(mpVideoID, value);
			}
		}
		else if (param.equals("jitterBuffer"))	{
			try {
				Integer val = Integer.parseInt(value);
				setJitterBuffer(val.intValue());
			} catch (NumberFormatException e) {
				log.warning("Parameter \"bandwidth\" must be integer value!");
			}
		}

		framePrevTimestamp = 0;
	}


	public void sendCommand(String command)
	{
		if (!isWorking) {
			log.severe("Sending command \"" + command + "\" when player is not ready");
			return;
		}

		log.info("sendCommand: " + command);

		if (command.equals("open"))	{
			if (stepMode == 1 && (status == STATUS_PLAY_SERVER || status == STATUS_PLAY_BUFFER)) {
				log.info("Not ready");
			} else {
				this.open(videoURL, streamOverTCP, streamOverHTTP, videoURL != null ? 0 : 1, maxHWFrameRate);
			}
		} else if (command.equals("play"))	{
			if (stepMode == 1 && (status == STATUS_PLAY_SERVER || status == STATUS_PLAY_BUFFER)) {
				log.info("Not ready");
			} else {
				clearResults();
				if (status != STATUS_OPENED)
					pauseSessions(false, false);
				playSessions();
			}
		} else if (command.equals("stop")) {
			clearResults();
			objsTracker = null;
			stopSessions();
			repaint();
		} else if (command.equals("pause"))	{
			clearResults();
			pauseSessions(true, false);
			repaint();
		} else if (command.equals("snapshot")) {
			saveSnapshot();
		} else if (command.equals("clear"))	{
			clear();
		}
	}


	private void playSessions()
	{
		if (videoURL == null && audioURL == null) {
			log.severe("Please specify 'videoURL' or 'audioURL' param");
		}
		if (videoSession == null && videoURL != null) {   
			videoSession = new VideoSession();
			videoSession.start();
		}
		if (audioSession == null && audioURL != null) {   
			audioSession = new AudioSession();
			audioSession.start();
		}
	}


	private void stopSessions()
	{
		// stop and pause are synchronized to prevent scenario when pause goes _after_ stop.
		// this can happened in step mode when user sent stop command, but frame is not yet
		// displayed
		synchronized (this) {
			if (mpVideoID != 0) 	{
				this.stop(mpVideoID);

				videoSession = null;
				mpVideoID = 0;
				videoPaused = false;
				serverVideoPaused = false;
			}

			if (mpAudioID != 0) {
				this.stop(mpAudioID);

				audioSession = null;
				mpAudioID = 0;
				audioPaused = false;
				serverAudioPaused = false;
			}

			if (audioLine != null) {
				try	{
					audioLine.close();
				} catch (Exception e) {
					log.warning(e.toString());
				}
			}
		}
		changeStatus(STATUS_STOPPED, false);
	}

	/**
	 * pauseSessions
	 */
	private void pauseSessions()
	{
		pauseSessions(true, true);
	}


	/**
	 * pauseSessions
	 * @param changeStatus		true if status change callback has to be invoked
	 * @param sendStatusNotification		true if invoke status change callback
	 */
	private void pauseSessions(boolean changeStatus, boolean sendStatusNotification)
	{
		synchronized (this) {
			if (mpVideoID != 0) {
				if (!serverVideoPaused) {
					this.pause(mpVideoID);
					videoSession = null;
					videoPaused = true;
					videoDisplayed = false;
				}
			}

			if (mpAudioID != 0)	{
				if (!serverAudioPaused)	{
					this.pause(mpAudioID);
					audioSession = null;
					audioPaused = true;
				}
			} 

			if (audioLine != null) {
				audioLine.flush();
			}
		}

		if (changeStatus && (mpVideoID != 0 || mpAudioID != 0)) {
			changeStatus(STATUS_IDLE, sendStatusNotification);
		}
	}


	private void jump2timestamp(int value)
	{
		if (mpVideoID != 0)	{
			this.jumpToTimestamp(mpVideoID, value);
		}

		if (mpAudioID != 0)	{
			this.jumpToTimestamp(mpAudioID, value);
		}
	
		videoDisplayed = false;
		if (audioLine != null)
			audioLine.flush();
	}


	private void changeSessionsSpeed(float value)
	{
		if (mpVideoID != 0)	{
			this.speed(mpVideoID, value);
		}

		if (mpAudioID != 0)	{
			this.speed(mpAudioID, value);
		}
	}
	
	private void changeStepMode(int value)
	{
		stepMode = value;
		if (mpVideoID != 0)	{
			this.stepMode(mpVideoID, value);
		}

		if (mpAudioID != 0)	{
			this.stepMode(mpAudioID, value);
		}
	}


	private void changeStatus(int status)
	{
		if (!isWorking) return;
		changeStatus(status, true);
	}

	public int getStatus()
	{
		return status;
	}
	
	private void changeStatus(int status, boolean sendStatusNotification)
	{      
		if (status != STATUS_STOPPED && this.status == status && (status != STATUS_IDLE || !sendStatusNotification)) return;

		log.info("changeStatus: " + status);

		this.status = status; 

		isConnecting = 0;

		if (status == STATUS_STOPPED) {
			if (mpVideoID != 0)	{
				stop(mpVideoID);
				videoSession = null; 
				mpVideoID = 0;
				videoPaused = false;
				serverVideoPaused = false;
			}
			if (mpAudioID != 0)	{
				audioSession = null; 
				mpAudioID = 0;
				audioPaused = false;
				serverAudioPaused = false;
			}

			if (audioLine != null) {
				try {
					audioLine.close();
				} catch (Exception e) {
					log.warning(e.toString());
				}
			}
			bufferStartTimestamp = 0;
			bufferEndTimestamp = 0;
			videoDisplayed = false;
		} else if (status == STATUS_IDLE) {
			// flushing data to sound device
			if (audioLine != null) {
				audioLine.flush();
			}
		}

		if (sendStatusNotification) {
			// adding status to queue.
			changeStatusEvent.add(status);
		}
	}


	private void saveSnapshot()
	{
		if (im != null && mpVideoID != 0 && snapshotFileName != null) {        	
			SaveSnapshotThread snapshotThread = new SaveSnapshotThread();
			snapshotThread.start();
		}
	}

	private void setMetadataTagInfo(long mpID, String info)
	{
		try {
			JsonValue v = ( new JsonLiteReader( info ) ).v();
			visibleMetadata.clear();
			for( int i = 0; i < v.size(); i++ ) {
				JsonValue obj = v.at( i );
				JsonValue tag = obj.at( "tag" );
				visibleMetadata.put( tag.asString(), obj.at( "view" ) );
			}
		}
		catch( Exception ex ) {
			log.warning( "MediaPlayer.setMetadataTagInfo: " + ex.toString() );
		}
	}

	/**
	 * Check if display ready to show image
	 */
	private int isDisplayReady()	// JNI call
	{
		return (displayBusy || !isWorking) ? 0 : 1;
	}

	private int canDisplayFrame(long secs, long usecs)	// JNI call
	{
		Date date = new Date(secs * 1000000 + usecs/1000);
		
		return listener.canDisplayFrame(date) ? 1 : 0;
	}
	
	private byte[] createRGB24Image(int width, int height, float pixelAspectRatio)
	{
		if (!this.isStretchPixelAspectRatio && this.pixelAspectRatio == 0f) {
			this.pixelAspectRatio = pixelAspectRatio;
			zoomControl.setPixelAspectRatio(pixelAspectRatio);
		}

		if (im == null || im.getWidth() != width || im.getHeight() != height) {
			im = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
			zoomControl.setIm(im);
		}

		im.setAccelerationPriority(1);
		clearScreenMargins.set(true);
		updatePaintParameters();

		videoDisplayed = true;
		// returning "pointer" to data to JNI
		return ((DataBufferByte)im.getRaster().getDataBuffer()).getData();
	}

	private void configureAudioDevice(int rate, int bits, int channels)
	{
		log.info("Configure audio device: rate=" + rate + " bits=" + bits + " channeld=" + channels);

		AudioFormat af = new AudioFormat(rate, 16, channels, true, false);

		DataLine.Info info = new DataLine.Info(SourceDataLine.class, af);
		try {
			audioLine = (SourceDataLine)AudioSystem.getLine(info);
			audioLine.open(af,2*rate*channels/10 /*100ms*/);
			audioLine.start();			
		} catch (LineUnavailableException e) {
			log.log(Level.WARNING, "Failed to get audio line", e);
		}
	}

	private void lockImageBuffer()
	{
		imageMutex.lock();
	}

	private void unlockImageBuffer()
	{
		imageMutex.unlock();
	}

	/**
	 * Signal from JNI that we have new frame to display
	 * 
	 * @param currentFrameSec Time of current frame in seconds (unix time)
	 * @param currentFrameUSec Time of current frame in microseconds
	 * @param bufferLBSec Time of first frame in the buffer (Left Boundary) in seconds (0 - if no buffer) 
	 * @param bufferLBUSec Time of first frame in the buffer (Left Boundary) in microseconds
	 * @param bufferRBSec Time of last frame in the buffer (Right Boundary) in seconds (0 - if no buffer) 
	 * @param bufferRBUSec Time of last frame in the buffer (Right Boundary) in microseconds
	 * @param bufferUsage in percents
	 * @param objsTrackData Tracked objects in binary format. null if none
	 */
	@SuppressWarnings("unused")
	private void displayFrame(int currentFrameSec, 
			int currentFrameUSec,    
			int bufferLBSec, 
			int bufferLBUSec,    
			int bufferRBSec, 
			int bufferRBUSec,     
			int bufferUsage,
			byte objsTrackData[])
	{
		videoDisplayed = true;
		
		frameTimestamp = currentFrameSec * 1000L + currentFrameUSec / 1000;
		bufferStartTimestamp = bufferLBSec * 1000L + bufferLBUSec / 1000;
		bufferEndTimestamp = bufferRBSec * 1000L + bufferRBUSec / 1000;

		if (!isWorking) return;

		if (im == null) {
			log.warning("[" + playerId + "] Buffered image was not created yet");
		} else {
			// Allowing no more than 4 callbacks per second
			// if (listener != null && (stepMode == 1 ||
			//                          frameTimestamp >= framePrevTimestamp + 250 ||
			//                          frameTimestamp <= framePrevTimestamp - 250 ))
			// {
			listener.newFrame(frameTimestamp, bufferStartTimestamp, bufferEndTimestamp, im);
			framePrevTimestamp = frameTimestamp;
			// }


			if (objsTrackData != null) {
				objsTracker = new ObjectsTracker(objsTrackData);
				/* only for debug purposes
                           System.out.print("objsTrackData[" + objsTrackData.length + "]:");
                           for(int i=0; i<objsTrackData.length; i++)
                           System.out.print(" " + Integer.toHexString(objsTrackData[i]));
                           System.out.println("");*/
			}

			isScreenCleared = false;
			errorMessage = null;
			signId = SIGN_NONE;
			if (isActive) {
				repaint();
			}
		}


		if (stepMode == 1 && mpVideoID != 0) pauseSessions();

	}


	/**
	 * playPCMFrame
	 * Called from JNI
	 */
	@SuppressWarnings("unused")
	private void playPCMFrame(byte[] data, int size,     		
			int currentFrameSec, 
			int currentFrameUSec,    
			int bufferLBSec, 
			int bufferLBUSec,    
			int bufferRBSec, 
			int bufferRBUSec,     
			int bufferUsage)
	{
		if (this.videoSession == null) {
			frameTimestamp = currentFrameSec * 1000L + currentFrameUSec / 1000;
			bufferStartTimestamp = bufferLBSec * 1000L + bufferLBUSec / 1000;
			bufferEndTimestamp = bufferRBSec * 1000L + bufferRBUSec / 1000;            	
		}

		if (isWorking && audioLine != null && !mute) {
			
			if (this.videoSession != null && videoDisplayed == false)
			{
				return;
			}
			
//			int available = audioLine.available();  				
//			log.warning("Writing to audio line " + size + " bytes " + "available: " + available + " size: " + audioLine.getBufferSize());
			
//			if (size > available) {
//				log.warning("size > available (" + size + " > " + available + ")");
////				if (available == 0) // flush line to prevent latency
////					audioLine.flush();
////				size = available;
//			}
			
			int bytesWritten = audioLine.write(data, 0, size);
			if (bytesWritten != size) {
				log.warning("bytesWritten != size (" + bytesWritten + " != " + size + ")");
			}

			if (this.videoSession == null) {
				// Allowing no more than 4 callbacks per second
				if (listener != null && (stepMode == 1 ||
						frameTimestamp >= framePrevTimestamp + 250 ||
						frameTimestamp <= framePrevTimestamp - 250 ))
				{
					//listener.newFrame(frameTimestamp, bufferStartTimestamp, bufferEndTimestamp, null);
					framePrevTimestamp = frameTimestamp;
				}

				if (stepMode == 1) pauseSessions();
			}
		}
	}


	@SuppressWarnings("unused")		// JNI call
	private void bufferChanged(    
			int bufferLBSec, 
			int bufferLBUSec,    
			int bufferRBSec, 
			int bufferRBUSec)
	{
		bufferStartTimestamp = bufferLBSec * 1000L + bufferLBUSec / 1000;
		bufferEndTimestamp = bufferRBSec * 1000L + bufferRBUSec / 1000;

		if (isWorking && listener != null) {
			listener.bufferChanged(bufferStartTimestamp, bufferEndTimestamp); 
		}
	}


	@SuppressWarnings("unused")		// JNI call	
	private void log(String msg)
	{
		log.info(msg);	
	}


	private void clear()
	{
		if (status == STATUS_INITIALIZED || status == STATUS_STOPPED) {
			frameTimestamp = 0; // reset frame time stamps
		}
		isScreenCleared = true;
		errorMessage = null;
		signId = SIGN_NONE;
		clearScreenMargins.set(true);
		repaint();
	}


	@SuppressWarnings("unused")		// JNI call
	private void setStreamType(String streamType)
	{
		this.streamType = streamType;
	}

	public String getStreamType()
	{
		return streamType;
	}

	public Container getOverlay()
	{
		return overlay;
	}

	@SuppressWarnings("unused")		// JNI call
	private void setResultMsg(String msg)
	{
		resultMsg = msg;
	}

	public String getResultMsg()
	{
		return resultMsg;
	}


	@SuppressWarnings("unused")		// JNI call
	private void setResultCode(int code)
	{
		resultCode = code;
	}

	public int getResultCode()
	{
		return resultCode;
	}

	private void clearResults()
	{
		resultCode = 0;
		resultMsg = "";
	}


	/**
	 * Print error message in the bottom part of image
	 * @param msg Error message
	 */
	public void printError(String msg)
	{
		errorMessage = msg;
		clearScreenMargins.set(true);
		repaint();
	}

	/**
	 * Display status sign
	 * @param signId id of sign
	 */
	public void displaySign(int signId)
	{
		this.signId = signId;
		clearScreenMargins.set(true);
		repaint();
	}

	// call outside of the applet
	// TA1561:
	public void showCenterSign(boolean showSign)
	{
		this.isCenterSignShown = showSign;
		clearScreenMargins.set(true);
		repaint();
	}

	/**
	 * @return video frame position within MediaPlayer component
	 */
	public Rectangle getFramePosition() {
		return this.screenRect;
	}

	/**
	 * @return time stamp of current frame or 0
	 */
	public long getCurrentTS() {
		return frameTimestamp;
	}

	public long getBufferStartTS() {
		return bufferStartTimestamp;
	}

	public long getBufferEndTS() {
		return bufferEndTimestamp;
	}
	
	private float calculateStretchPixelAspectRatio(int sourceWidth, int sourceHeight, int destWidth, int destHeight)
	{
		float factorSource = (float)sourceWidth / (float)sourceHeight;
		float factorDest = (float)destWidth / (float)destHeight;

		float aspectRatio = factorSource / factorDest;

		return aspectRatio;
	}

	//
	// Painting
	//
	
	public void paintComponent(Graphics g) 
	{
		int width = this.getWidth();
		int height = this.getHeight();

		if (!isWorking) 
			return;
		
		clearScreenMargins.set(true);
		
		if (im != null && !isScreenCleared)
		{
			if (true/*imageMutex.tryLock()*/)
			{
				try {
					if (clearScreenMargins.compareAndSet(true, false)) {
						g.setColor(getBackground());
						if (screenRect.x > 0) {
							// "tall" image					
							g.fillRect(0, 0, screenRect.x, height);
							g.fillRect(screenRect.x + screenRect.width, 0, width - (screenRect.x + screenRect.width), height);					
						} else if (screenRect.y > 0) {
							// "wide" image					
							g.fillRect(0, 0, width, screenRect.y);
							g.fillRect(0, screenRect.y + screenRect.height, width, height - (screenRect.y + screenRect.height));					
						}
					}
	
					if (zoomControl.isZoomed() || objsTracker != null) {
						// use backBufferImage for zooming and object tracking
	
						// all rectangles positioning is relative to screenRect
						// draw a part or full image to screen
						Rectangle imagePartRect = zoomControl.isZoomed() ? zoomControl.getPartRect() :
							new Rectangle(0, 0, im.getWidth(), im.getHeight());
	
						Graphics2D backBufferImageGraphics = (Graphics2D)backBufferImage.getGraphics();
	
						// draw image
						displayBusy = true;
						backBufferImageGraphics.drawImage(im, 0, 0, screenRect.width, screenRect.height, 
								imagePartRect.x, imagePartRect.y, imagePartRect.width, imagePartRect.height, null);
	
						if(objsTracker != null) {
							// Enable antialiasing for shapes
							backBufferImageGraphics.setRenderingHint(
									RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
	
							if(!zoomControl.isZoomed())
								objsTracker.parseAndDraw(backBufferImageGraphics);
							else {
								imagePartRect.width -= imagePartRect.x; imagePartRect.height -= imagePartRect.y;
	
								objsTracker.parseAndDraw(backBufferImageGraphics,
										new Rectangle(0, 0, screenRect.width, screenRect.height),
										imagePartRect, zoomControl.getImageZoomFactor());
							}
						}
	
						if (zoomControl.isZoomed()) {
							// draw small zoomed image
							zoomControl.paint(backBufferImageGraphics, objsTracker);
						}
	
						displayBusy = false;
						backBufferImageGraphics.dispose();
	
						g.drawImage(backBufferImage, screenRect.x, screenRect.y, null);
					} else {
						displayBusy = true;
						g.drawImage(im, screenRect.x, screenRect.y, screenRect.width, screenRect.height, null);
						displayBusy = false;
					}
				} finally {
					//imageMutex.unlock();
				}
			}
			else
			{
				System.err.println("could not lock");
			}
		}
		else {
			// clear player panel
			g.setColor(getBackground());
			g.fillRect(0, 0, width, height);
			clearScreenMargins.set(false);
		}

		// TA1561:
		if (!isScreenCleared) {
			if(signId != SIGN_NONE) {
				paintSign(g, signId, screenRect.x, screenRect.y, screenRect.width, screenRect.height);
			}
			if(isCenterSignShown) {
				paintCenterSign(g);
			}
		}

		overlay.setBounds(screenRect.x, screenRect.y, screenRect.width, screenRect.height);
		this.revalidate();
		
		// print error message (if one exists)
		if (errorMessage != null) {
			paintMessage(g, errorMessage);
		}
	}


	protected void paintSign(Graphics g, int signId, int imX, int imY, int imW, int imH) {
		if (signId == SIGN_RED_CROSS) {
			int xwidth, x, y;
			if (imW > 0) {
				// use image size
				xwidth = Math.min(imW / 2, imH / 2);
				x = imX;
				y = imY;
			} else {
				// use applet size
				xwidth = Math.min(getWidth() / 2, getHeight() / 2);
				x = 0;
				y = 0;
			}

			int xheight = xwidth;
			int xcw = (xwidth <= 240 / 10) ? 1 : xwidth * 10 / 240;
			int xch = xcw;

			Polygon x1 = new Polygon();
			x1.addPoint(x + xcw, y + 0);
			x1.addPoint(x + xwidth, y + xheight - xch);
			x1.addPoint(x + xwidth - xcw, y + xheight);
			x1.addPoint(x + 0, y + xch);

			Polygon x2 = new Polygon();
			x2.addPoint(x + 0, y + xheight - xch);
			x2.addPoint(x + xwidth - xcw, y + 0);
			x2.addPoint(x + xwidth, y + xch);
			x2.addPoint(x + xcw, y + xheight);

			g.setColor(Color.RED);
			g.fillPolygon(x1);
			g.fillPolygon(x2);
		}
	}


	// TA1561:    
	protected void paintCenterSign(Graphics g) {
		g.setColor(Color.RED);
		int xc = Math.round(getWidth() / 2);
		int yc = Math.round(getHeight() / 2);
		int w2 = 16;
		g.drawRect(xc-w2, yc-w2, 2*w2, 2*w2);
		g.drawLine(xc-w2/2, yc, xc-w2/2-w2, yc);
		g.drawLine(xc+w2/2, yc, xc+w2/2+w2, yc);
		g.drawLine(xc, yc-w2/2, xc, yc-w2/2-w2);
		g.drawLine(xc, yc+w2/2, xc, yc+w2/2+w2);
	}


	protected void paintMessage(Graphics g, String msg) {
		final int textBoxMargin = 3;
		final int textBoxArcMargin = textBoxMargin * 4;
		FontMetrics fm = getFontMetrics(messageFont);
		int textY = getHeight() - textBoxMargin*2 - fm.getDescent();
		int textX = (getWidth() - fm.stringWidth(msg))/2;
		if (textX < 1) { // If string too long, start at 0
			textX = 1;
		}

		// draw transparent block with black border
		int boxX = textX/3*2;
		int boxY = getHeight() - fm.getHeight() - textBoxMargin*3;

		Graphics2D g2 = (Graphics2D)g;
		Composite originalComposite = g2.getComposite();
		g2.setColor(Color.WHITE);
		g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7f));
		g2.fillRoundRect(boxX, boxY, getWidth() - boxX*2, fm.getHeight() + textBoxMargin*2, textBoxArcMargin, textBoxArcMargin);
		g2.setComposite(originalComposite);
		g2.setColor(Color.BLACK);
		g2.drawRoundRect(boxX, boxY, getWidth() - boxX*2, fm.getHeight() + textBoxMargin*2, textBoxArcMargin, textBoxArcMargin);

		// draw message
		g2.setColor(Color.RED);
		g2.setFont(messageFont);
		g2.drawString(msg, textX, textY);
	}


	/*
	 * ComponentListener implementation
	 */
	public void componentResized(ComponentEvent e) {
		updatePaintParameters();
	}

	public void componentMoved(ComponentEvent e) {}
	public void componentShown(ComponentEvent e) {}
	public void componentHidden(ComponentEvent e) {}

	private void updatePaintParameters() {
		if (im == null)
		{
			return;
		}

		if (this.isStretchPixelAspectRatio)
		{
			this.pixelAspectRatio = this.calculateStretchPixelAspectRatio(im.getWidth(), im.getHeight(), this.getWidth(), this.getHeight());
			zoomControl.setPixelAspectRatio(this.pixelAspectRatio);
		}

		int canvasWidth = getWidth();
		int canvasHeight = getHeight();

		float appletAspectRatio = (float)canvasWidth / (float)canvasHeight;
		float imageAspectRatio = (float)im.getWidth() / (float)im.getHeight();

		imageAspectRatio /= pixelAspectRatio;

		if (imageAspectRatio <= appletAspectRatio)
		{
			// "wide" image
			screenRect.height = canvasHeight;
			screenRect.width = Math.round(canvasHeight * imageAspectRatio);
			screenRect.x = (canvasWidth - screenRect.width) / 2;
			screenRect.y = 0;
		} else {
			// "tall" image
			screenRect.width = canvasWidth;
			screenRect.height = Math.round(canvasWidth / imageAspectRatio);
			screenRect.x = 0;
			screenRect.y = (canvasHeight - screenRect.height) / 2;
		}

		if (backBufferImage == null || backBufferImage.getWidth() != screenRect.width || backBufferImage.getHeight() != screenRect.height)
		{
			backBufferImage = new BufferedImage(screenRect.width, screenRect.height, BufferedImage.TYPE_4BYTE_ABGR);
		}

		zoomControl.setScreenRect(screenRect);
		zoomControl.calculateImageRectSize();
	}


	/**
	 * Class VideoSession
	 */
	class VideoSession
	{
		public VideoSession() {
		}

		public void start() {
			if (videoPaused == true && status != STATUS_OPENED) {
				videoPaused = false;
				serverVideoPaused = false;

				MediaPlayer.this.resume(mpVideoID, direction);   
			} else	{
				isConnecting = 1;
				//changeStatus(MediaPlayer.STATUS_PLAY_SERVER);
				MediaPlayer.this.play(mpVideoID);
			}    
		}
	}


	/**
	 * Class AudioSession
	 */
	class AudioSession
	{
		public AudioSession() {
		}

		public void start() {
			if (audioPaused == true && status != STATUS_OPENED) {
				audioPaused = false;
				serverAudioPaused = false;

				MediaPlayer.this.resume(mpAudioID, direction); 
			} else {
				isConnecting = 1;
				//changeStatus(MediaPlayer.STATUS_PLAY_SERVER);
				MediaPlayer.this.play(mpAudioID);
			}
		}
	}


	/**
	 * class ChangeStatusEvent
	 */
	class ChangeStatusEvent extends Thread 
	{
		private SynchronousQueue<Integer> changeStatusQueue	= new SynchronousQueue<Integer>(true);
		private Object readySync = new Object();

		public void start() {
			synchronized(readySync) {
				super.start();
			}
		}

		public void add(int status) {
			try {
				changeStatusQueue.put(new Integer(status));
			} catch (InterruptedException e) {}
		}

		public void run() {
			// Tell external client that player is ready to 
			// get parameters and commands
			synchronized(readySync)	{
				if (listener != null) {
					listener.statusChanged(STATUS_INITIALIZED, resultCode, resultMsg);
				}
			}

			int status;			
			while (true) {
				try {
					// Calling "onStatusChange"					
					status = changeStatusQueue.take();
					if (status == Integer.MIN_VALUE) {
						break;
					} else if (listener != null) {
						listener.statusChanged(status, resultCode, resultMsg);
					}
				} catch (InterruptedException ex) {}
			}
		}

		public void done() {
			try {
				changeStatusQueue.put(new Integer(Integer.MIN_VALUE));
			} catch (InterruptedException e) {}        	
		}
	}


	/**
	 * class SaveSnapshotThread
	 */
	class SaveSnapshotThread extends Thread
	{
		private String selectFileName()
		{
			FileDialog fd = null;


			Container c = MediaPlayer.this.getParent();
			while(c != null) {
				if (c instanceof Frame) {
					fd = new FileDialog((Frame)c, "Save snapshot", FileDialog.SAVE);
					break;
				} else if (c instanceof Dialog) {
					fd = new FileDialog((Dialog)c, "Save snapshot", FileDialog.SAVE);
					break;
				} else 
					c = c.getParent();
			}

			if (fd == null)
			{
				log.severe("[" + playerId + "] Failed to get parent component to display File input dialog");
				return null;
			}

			String fileName;
			if (System.getProperty("os.name").startsWith("Windows") && false) {
				fileName = snapshotFileName + ".%unused%";
			} else {
				fileName = snapshotFileName;
			}

			fd.setFile(fileName);
			fd.setVisible(true);

			if (fd.getFile() != null) {

				fileName = fd.getDirectory() + fd.getFile().replaceAll(".%unused%", "");
				if (!fileName.endsWith(".jpg")) {
					fileName += ".jpg";
				}

				return fileName;
			}
			else return null;
		}


		public void run()
		{
			String fileName = snapshotFileName;

			if (MediaPlayer.this.showSnapshotDialog)
				fileName = selectFileName();

			if (fileName != null) {
				try {
					ByteArrayOutputStream data;
					data = new ByteArrayOutputStream();
					
					ImageWriter writer = ImageIO.getImageWritersByFormatName("jpeg").next();
					ImageWriteParam param = writer.getDefaultWriteParam();
					param.setCompressionQuality((float) 75 / 100.0f);
					writer.setOutput(data);
					writer.write(im);
					
					/*JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(data);
					JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(im);

					param.setQuality((float) 75 / 100.0f, false);
					encoder.setJPEGEncodeParam(param);
					encoder.encode(im);*/

					saveCurrentVideoFrame(fileName, data.toByteArray());
				/*} catch (ImageFormatException e) {
					e.printStackTrace();*/
				} catch (IOException e) {
					e.printStackTrace();
				}
			}

			if (listener != null)
				listener.snapshotFinished();
		}
	}


	/**
	 * class ObjectsTracker
	 */
	class ObjectsTracker implements Drawable
	{
		private byte[] bitstream;
		private int offset;
		private int length;
		private int bufa;
		private int bufb;
		private int position = 0;

		private Rectangle dstRect;
		private Rectangle srcRect;
		private float  zoomFactor;
		private float  kW;
		private float  kH;

		private int objID = -1;
		private int objDrawType = 0;
		private int objXcoord = 0;
		private int objYcoord = 0;
		private int objWidth = 0;
		private int objHeight = 0;
		private int objColor = -1;
		private int objEndColor = -1;
		private int objLineWidth = 2;
		private int arrowHead = 8;
		private String text = null;
		private int xPoints[];
		private int yPoints[];
		private int nPoints;
		private float xTripPoints[];
		private float yTripPoints[];

		// Objects tracking start codes
		private final static byte OBJ_SEQUENCE_START_CODE      = 0x0A;
		private final static byte OBJ_START_CODE               = 0x01;
		private final static byte OBJ_DRAW_TYPE_START_CODE     = 0x02;
		private final static byte OBJ_X_COORD_START_CODE       = 0x03;
		private final static byte OBJ_Y_COORD_START_CODE       = 0x04;
		private final static byte OBJ_HEIGHT_START_CODE        = 0x05;
		private final static byte OBJ_WIDTH_START_CODE         = 0x06;
		private final static byte OBJ_COLOR_START_CODE         = 0x07;
		private final static byte OBJ_LINEWIDTH_START_CODE     = 0x08;
		private final static byte OBJ_TEXT_START_CODE          = 0x09;
		private final static byte OBJ_POLYDATA_START_CODE      = 0x0B;
		private final static byte OBJ_ARROWHEADSIZE_START_CODE = 0x0C;
		private final static byte OBJ_SEQUENCE_END_START_CODE  = 0x7F;

		private final static byte OBJ_DRAW_TYPE_BOX            = 0x01;
		private final static byte OBJ_DRAW_TYPE_MLINE          = 0x02;
		private final static byte OBJ_DRAW_TYPE_TEXT           = 0x03;
		private final static byte OBJ_DRAW_TYPE_TIMESTAMP      = 0x04;
		private final static byte OBJ_DRAW_TYPE_POLYGON        = 0x05;
		private final static byte OBJ_DRAW_TYPE_TRIPWIRE_ARROW = 0x06;
		private final static byte OBJ_DRAW_TYPE_ARROW          = 0x07;
		private final static byte OBJ_DRAW_TYPE_OVAL           = 0x08;

		private final static int METADATA_TAGID = 0xffff;


		public ObjectsTracker(byte[] bitstream)
		{
			this.bitstream = bitstream;
			//this.bitstream = new byte[bitstream.length];
			//System.arraycopy(bitstream, 0, this.bitstream, 0, bitstream.length);
			xTripPoints = new float [2];
			yTripPoints = new float [2];
		}

		private void skipBits(final int bits)
		{
			position += bits;

			if (position >= 32)
			{
				bufa = bufb;
				try
				{
					bufb =  (bitstream[offset + 0] & 0xff) << 24;
					bufb |= (bitstream[offset + 1] & 0xff) << 16;
					bufb |= (bitstream[offset + 2] & 0xff) <<  8;
					bufb |= (bitstream[offset + 3] & 0xff);
				}
				catch (ArrayIndexOutOfBoundsException ignored)
				{  
					// log("exeception, offset=" + offset);
				}

				offset += 4;
				position -= 32;
			}
		}

		private int showBits(final int bits)
		{                  
			if (offset > length+8)
			{
				log.warning("[" + playerId + "] Broken objects data!!!");
				return -1;
			}
			int nbit = (bits + position) - 32;

			if (nbit > 0)
			{
				return ((bufa & (0xffffffff >>> position)) << nbit) | (bufb >>> (32 - nbit));
			}
			else
			{
				return (bufa & (0xffffffff >>> position)) >>> (32 - position - bits);
			}
		}

		private int getBits(final int n) 
		{
			int ret = showBits(n);

			skipBits(n);

			return ret;
		}

		private String getText()
		{
			int size = getBits(8);

			byte[] text = new byte[size];

			for (int i = 0; i < size; i++)
			{
				text[i] = (byte)getBits(8);
			}

			return new String(text);
		}

		private void fillPolygonData()
		{
			nPoints = getBits(8);
			xPoints = new int [nPoints];
			yPoints = new int [nPoints];

			for (int i = 0; i < nPoints; i++)
			{
				xPoints[i] = getBits(16);
				yPoints[i] = getBits(16);
			}

			if(nPoints == 2) // trip wire ?
			{
				for(int i=0; i < nPoints; i++)
				{

					xTripPoints[i] = xPoints[i];
					yTripPoints[i] = yPoints[i];
				}
			}
		}

		/**
		 * Draw analytics objects over small scaled im image on the screenRect entirely
		 */
		public void parseAndDraw(Graphics2D g2)
		{
			parseAndDraw(g2, new Rectangle(screenRect.width, screenRect.height),
					new Rectangle(im.getWidth(), im.getHeight()), zoomControl.getImageZoomFactor());
		}

		/**
		 * Draw analytics objects over small scaled im image at any place of the screenRect
		 */
		public void draw(Graphics g, Rectangle dstRect)
		{
			parseAndDraw((Graphics2D)g, dstRect, new Rectangle(im.getWidth(), im.getHeight()),
					screenRect.width / (float)im.getWidth()); // minImageZoomFactor (line width calculation)
		}

		/**
		 * Draw analytics objects over some part of zoomed im image at any place of the destination screenRect
		 */		
		public void parseAndDraw(Graphics2D g2, Rectangle dstRect, Rectangle srcRect, float zoomFactor)
		{
			this.dstRect = dstRect;
			this.srcRect = srcRect;
			this.zoomFactor = zoomFactor;

			this.kW = dstRect.width  / (float)srcRect.width;
			this.kH = dstRect.height / (float)srcRect.height;		
			/*
			// Clear image with transparent alpha by drawing a rectangle
			Composite originalComposite = g2.getComposite();
			g2.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));
			g2.fill(new Rectangle(0, 0, screenRect.width, screenRect.height));
			g2.setComposite(originalComposite);
			 */
			objID  = objEndColor = -1;
			offset = position = 0;
			length = bitstream.length;

			try
			{
				bufa  = (bitstream[offset + 0] & 0xff) << 24;
				bufa |= (bitstream[offset + 1] & 0xff) << 16;
				bufa |= (bitstream[offset + 2] & 0xff) <<  8;
				bufa |= (bitstream[offset + 3] & 0xff);

				bufb  = (bitstream[offset + 4] & 0xff) << 24;
				bufb |= (bitstream[offset + 5] & 0xff) << 16;
				bufb |= (bitstream[offset + 6] & 0xff) <<  8;
				bufb |= (bitstream[offset + 7] & 0xff);
			}
			catch (ArrayIndexOutOfBoundsException e)
			{
				// bufa and bufb have been filled as much as they can be...
			}
			offset += 8;

			boolean done = false;
			boolean bEndColor = false;
			int tagYPos = 7;
			while (!done)
			{
				switch (getBits(8)) 
				{
				case OBJ_SEQUENCE_START_CODE:
					//                         log("OBJ_SEQUENCE_START_CODE\n");
					break;

				case OBJ_START_CODE:			
					//                          log("OBJ_START_CODE\n");			
					if( objID == METADATA_TAGID ) {
						if( visibleMetadata.containsKey( ""+objYcoord ) ) {
							objYcoord = tagYPos;
							objXcoord = 3;
							objHeight = ( im.getHeight() + 12 ) / 25;
							tagYPos += objHeight + 2;
						} else {
							objID = -1;
						}
					}
					if (objID != -1) drawObject(g2);						
					objID = getBits(16);
					// log("objID=" + objID + "\n");
					objLineWidth = 2;
					arrowHead = 8;
					bEndColor = false;
					break;

				case OBJ_DRAW_TYPE_START_CODE:
					//                          log("OBJ_DRAW_TYPE_START_CODE\n");
					objDrawType = getBits(8);
					//                          log("obj type: " + objDrawType + "\n");

					break;

				case OBJ_X_COORD_START_CODE:
					//                          log("OBJ_X_COORD_START_CODE\n");
					objXcoord = getBits(16);
					break;	

				case OBJ_Y_COORD_START_CODE:
					//                          log("OBJ_Y_COORD_START_CODE\n");
					objYcoord = getBits(16);
					break;		

				case OBJ_HEIGHT_START_CODE:
					//                          log("OBJ_HEIGHT_START_CODE\n");
					objHeight = getBits(16);
					break;	

				case OBJ_WIDTH_START_CODE:
					//                          log("OBJ_WIDTH_START_CODE\n");
					objWidth = getBits(16);
					//                          log("objWidth = " + objWidth + "\n");
					break;	

				case OBJ_COLOR_START_CODE:
					//                          log("OBJ_COLOR_START_CODE\n");
					if(bEndColor) {
						objEndColor = getBits(32);
						break;
					}
					objColor = getBits(32);
					bEndColor = true;
					break;	

				case OBJ_LINEWIDTH_START_CODE:
					//                          log("OBJ_LINEWIDTH_START_CODE\n");
					objLineWidth = getBits(8);
					break;	

				case OBJ_ARROWHEADSIZE_START_CODE:
					//                          log("OBJ_LINEWIDTH_START_CODE\n");
					arrowHead = getBits(8);
					break;	

				case OBJ_TEXT_START_CODE:
					//                          log("OBJ_TEXT_START_CODE\n");                                              
					text = getText();
					break;

				case OBJ_POLYDATA_START_CODE:
					fillPolygonData();
					break;

				case OBJ_SEQUENCE_END_START_CODE:					
					//                          log("OBJ_SEQUENCE_END_START_CODE\n");
					// UAS metadata hardcode
					if( objID == METADATA_TAGID ) {
						if( visibleMetadata.containsKey( ""+objYcoord ) ) {
							objYcoord = tagYPos;
							objXcoord = 3;
							objHeight = ( im.getHeight() + 12 ) / 25;
							tagYPos += objHeight + 2;
						} else {
							objID = -1;
						}
					}
					if (objID != -1) drawObject(g2);
					done = true;
					break;
				}
			}
			//                 log("========== DONE ============\n");
		}

		private void drawObject(Graphics2D g2)
		{
			Stroke origStroke = g2.getStroke();
			int stroke = (int)(zoomFactor * objLineWidth);
			Composite originalComposite = g2.getComposite();
			g2.setColor(new Color(objColor));
			g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7f));

			switch (objDrawType) 
			{
			case OBJ_DRAW_TYPE_BOX:
				g2.setStroke(new BasicStroke((stroke < 1) ? 1 : stroke));
				g2.drawRect((int)(dstRect.x + kW*(objXcoord - srcRect.x - objWidth/2.f) + 0.5),
						(int)(dstRect.y + kH*(objYcoord - srcRect.y - objHeight/2.f) + 0.5),
						(int)(kW*objWidth + 0.5), (int)(kH*objHeight + 0.5));
				g2.setStroke(origStroke);
				break;

			case OBJ_DRAW_TYPE_OVAL:
				g2.setStroke(new BasicStroke((stroke < 1) ? 1 : stroke));
				g2.drawOval((int)(dstRect.x + kW*(objXcoord - srcRect.x - objWidth/2.f) + 0.5),
						(int)(dstRect.y + kH*(objYcoord - srcRect.y - objHeight/2.f) + 0.5),
						(int)(kW*objWidth + 0.5), (int)(kH*objHeight + 0.5));
				g2.setStroke(origStroke);
				break;					

			case OBJ_DRAW_TYPE_MLINE:
			case OBJ_DRAW_TYPE_POLYGON:
				for (int i = 0; i < nPoints; i++)
				{
					xPoints[i] = (int)(dstRect.x + kW*(xPoints[i] - srcRect.x) + 0.5);
					yPoints[i] = (int)(dstRect.y + kH*(yPoints[i] - srcRect.y) + 0.5);
				}

				if(objDrawType == OBJ_DRAW_TYPE_MLINE) {
					g2.setStroke(new BasicStroke((stroke < 1) ? 1 : stroke));
					if(objColor >= 0 && objEndColor >= 0) {
						Color startColor = new Color(objColor);
						Color endColor = new Color(objEndColor);
						Color eColor = startColor;
						for(int i=1; i<nPoints; i++) {
							Color sColor = eColor;
							eColor = new Color(startColor.getRed() + (int)((endColor.getRed() - startColor.getRed())*(i/(float)(nPoints-1))),
									startColor.getGreen() + (int)((endColor.getGreen() - startColor.getGreen())*(i/(float)(nPoints-1))),
									startColor.getBlue() + (int)((endColor.getBlue() - startColor.getBlue())*(i/(float)(nPoints-1))));
							GradientPaint gradient = new GradientPaint(xPoints[i-1], yPoints[i-1], sColor, xPoints[i], yPoints[i], eColor);
							g2.setPaint(gradient);
							g2.drawLine(xPoints[i-1], yPoints[i-1], xPoints[i], yPoints[i]);
						}
					} else
						g2.drawPolyline(xPoints, yPoints, nPoints);
					g2.setStroke(origStroke);
				} else
					g2.fillPolygon(xPoints, yPoints, nPoints);
				objEndColor = -1;
				break;

			case OBJ_DRAW_TYPE_TEXT:

				if (text != null)
				{
					Font font = new Font("Serif", Font.BOLD, (int)(kH*objHeight));                                   
					FontMetrics fm = getFontMetrics(font);                                            
					g2.setFont(font);
					g2.drawString(text, (int)(dstRect.x + kW*(objXcoord - srcRect.x) + 0.5),
							(int)(dstRect.y + kH*(objYcoord - srcRect.y + fm.getAscent() - fm.getDescent()) + 0.5));                                        
				}
				break;

			case OBJ_DRAW_TYPE_TIMESTAMP:
				break;	

			case OBJ_DRAW_TYPE_ARROW:
				if(xTripPoints.length < 2 || yTripPoints.length < 2)
					break;

				for (int i = 0; i < 2; i++)
				{
					xTripPoints[i] = dstRect.x + kW*(xTripPoints[i] - srcRect.x);
					yTripPoints[i] = dstRect.y + kH*(yTripPoints[i] - srcRect.y);
				}

				// Orthogonal is [-b a]
				// Draw an arrow
				double size = Math.sqrt(Math.pow(xTripPoints[1] - xTripPoints[0], 2)
						+ Math.pow(yTripPoints[1] - yTripPoints[0], 2)),
						subsample = (kW < kH) ? kW : kH,
								a = (xTripPoints[1] - xTripPoints[0]) * kW / size,
								b = (yTripPoints[1] - yTripPoints[0]) * kH / size,
								tmpX = (xTripPoints[0] + xTripPoints[1]) /* subsample*/ / 2,
								tmpY = (yTripPoints[0] + yTripPoints[1]) /* subsample*/ / 2;
				int[] x_points = new int[3], y_points = new int[3];
				x_points[0] = (int)(tmpX - arrowHead * b + 0.5);
				y_points[0] = (int)(tmpY + arrowHead * a + 0.5);
				x_points[1] = (int)(tmpX + arrowHead * b + 0.5);
				y_points[1] = (int)(tmpY - arrowHead * a + 0.5);
				x_points[2] = (int)(tmpX + arrowHead * a + 0.5);
				y_points[2] = (int)(tmpY + arrowHead * b + 0.5);

				g2.fillPolygon(x_points, y_points, 3);
				break;

			case OBJ_DRAW_TYPE_TRIPWIRE_ARROW:
				if(xTripPoints.length < 2 || yTripPoints.length < 2)
					break;

				for (int i = 0; i < 2; i++)
				{
					xTripPoints[i] = dstRect.x + kW*(xTripPoints[i] - srcRect.x);
					yTripPoints[i] = dstRect.y + kH*(yTripPoints[i] - srcRect.y);
				}

				// Orthogonal is [-b a]
				// Draw an arrow
				double[] x_tmp = new double[4], y_tmp = new double[4];
				/*double*/ size = Math.sqrt(Math.pow(xTripPoints[1] - xTripPoints[0], 2)
						+ Math.pow(yTripPoints[1] - yTripPoints[0], 2));
				subsample = (kW < kH) ? kW : kH;
				a = kW*(xTripPoints[1] - xTripPoints[0]) / size;
				b = kH*(yTripPoints[1] - yTripPoints[0]) / size;
				tmpX = (xTripPoints[0] + xTripPoints[1]) /* subsample*/ / 2;
				tmpY = (yTripPoints[0] + yTripPoints[1]) /* subsample*/ / 2;
				x_tmp[0] = tmpX - 10 * b;    y_tmp[0] = tmpY + 10 * a;
				x_tmp[1] = tmpX + 10 * b;    y_tmp[1] = tmpY - 10 * a;
				x_tmp[2] = x_tmp[1] + 4 * a; y_tmp[2] = y_tmp[1] + 4 * b;
				x_tmp[3] = x_tmp[0] + 4 * a; y_tmp[3] = y_tmp[0] + 4 * b;

				/*int[]*/ x_points = new int[7]; y_points = new int[7];
				for(int i=0; i<x_tmp.length; i++) {
					x_points[i] = (int)(x_tmp[i] - 0.5);
					y_points[i] = (int)(y_tmp[i] - 0.5);
				}
				x_points[4] = (int)(x_tmp[3] - 2 * a + a * arrowHead - 0.5);
				y_points[4] = (int)(y_tmp[3] - 2 * b + b * arrowHead - 0.5);
				x_points[5] = (int)(x_tmp[3] - 2 * a - b * arrowHead - 0.5);
				y_points[5] = (int)(y_tmp[3] - 2 * b + a * arrowHead - 0.5);
				x_points[6] = (int)(x_tmp[3] - 2 * a - a * arrowHead - 0.5);
				y_points[6] = (int)(y_tmp[3] - 2 * b - b * arrowHead - 0.5);

				g2.fillPolygon(x_points, y_points, 7);
				break;
			}

			g2.setComposite(originalComposite);
		}

	}

	public enum JsonValueType { jsNull, jsString, jsArray, jsObject }

	class JsonValue
	{
		private JsonValueType type;
		private int arraySize = 0;
		private Object value;

		public JsonValue()
		{
			type = JsonValueType.jsNull;
		}

		public JsonValue( String text )
		{
			type = JsonValueType.jsString;
			value = text;
		}

		public JsonValue( JsonValueType Type )
		{
			type = Type;
			switch( type ) {
			case jsString:
				value = "";
				break;
			case jsArray:
				value = new Hashtable();
				break;
			case jsObject:
				value = new Hashtable();
				break;
			default:
				type = JsonValueType.jsNull;
				value = null;
				break;
			}
		}

		public boolean isNull()
		{
			return type == JsonValueType.jsNull;
		}

		public boolean isArray()
		{
			return type == JsonValueType.jsArray;
		}

		public boolean isObject()
		{
			return type == JsonValueType.jsObject;
		}

		public boolean isString()
		{
			return type == JsonValueType.jsString;
		}

		public String asString() throws Exception
		{
			if( isString() ) {
				return (String) value;
			}
			throw new Exception( "json asString - not a string" );
		}

		public void set( String text )
		{
			type = JsonValueType.jsString;
			value = text;
		}

		public JsonValue at( int index ) throws Exception
		{
			if( isArray() ) {
				Hashtable ht = (Hashtable) value;
				if( index + 1 > arraySize )
					arraySize = index + 1;
				if( ht.containsKey( index ) ) {
					return (JsonValue) ht.get( index );
				} else {
					return (JsonValue) ht.put( index, new JsonValue() );
				}
			}
			throw new Exception( "json at - not an array" );
		}

		public JsonValue at( String index ) throws Exception
		{
			if( isObject() ) {
				Hashtable ht = (Hashtable) value;
				if( ht.containsKey( index ) ) {
					return (JsonValue) ht.get( index );
				} else {
					return (JsonValue) ht.put( index, new JsonValue() );
				}
			}
			throw new Exception( "json at - not an object" );
		}

		public JsonValue put( int index, JsonValue v ) throws Exception
		{
			if( isArray() ) {
				Hashtable ht = (Hashtable) value;
				if( index + 1 > arraySize )
					arraySize = index + 1;
				return (JsonValue) ht.put( index, v );
			}
			throw new Exception( "json put - not an array" );
		}

		public JsonValue put( String index, JsonValue v ) throws Exception
		{
			if( isObject() ) {
				Hashtable ht = (Hashtable) value;
				return (JsonValue) ht.put( index, v );
			}
			throw new Exception( "json put - not an object" );
		}

		public boolean exists( String index ) throws Exception
		{
			if( isObject() ) {
				Hashtable ht = (Hashtable) value;
				return ht.containsKey( index );
			}
			throw new Exception( "json exists - not an object" );
		}

		public int size() throws Exception
		{
			if( isArray() ) {
				return arraySize;
			}
			throw new Exception( "json size - not an array" );
		}
	}

	/**
	 * class JsonLiteReader
	 */
	class JsonLiteReader
	{
		private String json;
		private int idx;
		private JsonValue value;

		private JsonLiteReader( String jsonText ) throws Exception
		{
			json = jsonText;
			idx = 0;
			value = eatNext();
		}

		public JsonValue v()
		{
			return value;
		}

		private void eatWhitespace()
		{
			while( " \t\n\r".indexOf( json.charAt( idx ) ) >= 0 )
				idx++;
		}

		private JsonValue eatNext() throws Exception
		{
			eatWhitespace();
			JsonValue value = null;
			int cnt = 0;
			switch( json.charAt( idx ) ) {
			case '"':
				value = eatString();
				break;
			case '[':
				idx++; cnt = 0;
				value = new JsonValue( JsonValueType.jsArray );
				eatWhitespace();
				while( json.charAt( idx ) != ']' ) {
					value.put( cnt++, eatNext() );
					eatWhitespace();
					if( json.charAt( idx ) == ',' ) {
						idx++;
					} else {
						break;
					}
				}
				if( json.charAt( idx ) == ']' ) {
					idx++;
				} else {
					throw new Exception( "array, at " + idx );
				}
				break;
			case '{':
				idx++; cnt = 0;
				value = new JsonValue( JsonValueType.jsObject );
				eatWhitespace();
				while( json.charAt( idx ) != '}' ) {
					eatWhitespace();
					JsonValue key = eatString();
					eatWhitespace();
					if( json.charAt( idx ) != ':' ) {
						throw new Exception( "object, ':' expected but '" + json.charAt( idx ) + "' found, at " + idx );
					}
					idx++;
					eatWhitespace();
					value.put( key.asString(), eatNext() );
					eatWhitespace();
					if( json.charAt( idx ) == ',' ) {
						idx++;
					} else {
						break;
					}
				}
				if( json.charAt( idx ) == '}' ) {
					idx++;
				} else {
					throw new Exception( "object, at " + idx );
				}
				break;
			default:
				throw new Exception( "unknown, at " + idx );
			}
			return value;
		}

		private JsonValue eatString() throws Exception
		{
			StringBuilder sb = new StringBuilder();
			if( json.charAt( idx ) != '"' ) {
				throw new Exception( "object, ':' expected but '" + json.charAt( idx ) + "' found, at " + idx );
			}
			idx++;
			int start = idx;
			while( json.charAt( idx ) != '"' ) {
				if( json.charAt( idx ) == '\\' ) {
					idx++;
					switch( json.charAt( idx ) ) {
					case 'b':
						sb.append( '\b' );
						break;
					case 'f':
						sb.append( '\f' );
						break;
					case 'n':
						sb.append( '\n' );
						break;
					case 'r':
						sb.append( '\r' );
						break;
					case 't':
						sb.append( '\t' );
						break;
					case 'u':
						idx++;
						String hex = json.substring( idx, idx + 4 ).toUpperCase();
						if( !hex.matches( "[0-9A-F]{4}" ) ) {
							throw new Exception( "\\u not followed by 4 hex-digits: " + hex );
						}
						idx += 3; // idx++ below
						sb.append( (char) Integer.parseInt( hex, 16 ) );
						break;
					default:
						sb.append( json.charAt( idx ) );
						break;
					}
				} else {
					sb.append( json.charAt( idx ) );
				}
				idx++;
			}
			JsonValue v = new JsonValue( sb.toString() );
			idx++;
			return v;
		}
	}
}
