/**
 * VideoCapture - display live video from a capture source
 * Copyright 2006 Serious Cybernetics
 * Licensed under the GNU GPL version 2, see COPYING.txt
 * 2006-05-08 Initial version
 * 2006-05-10 Implement driver and format selection menus
 * 2006-05-12 Resize window to suit capture format 
 * @author Andrew Pam &lt;andrew@sericyb.com.au&gt;
 * @version 0.3
 */

import java.awt.*;
import java.awt.event.*;
import java.util.Iterator;
import java.util.Vector;

import javax.media.*;
import javax.media.format.*;
import javax.media.protocol.CaptureDevice;
import javax.media.protocol.DataSource;

import com.sun.media.format.AviVideoFormat;

public class VideoCapture extends Frame implements ActionListener,
		ControllerListener {

	private static final long serialVersionUID = -4253940057669754649L;

	CaptureDeviceInfo cdi = null;
	Vector devices, formats;
	CaptureDevice captureDevice;
	VideoFormat videoFormat;
	Menu menuDevice, menuFormat;
	Player player;

	/**
	 * @param title - Frame title
	 * @throws HeadlessException
	 */
	public VideoCapture(String title) throws HeadlessException {
		super(title);
		addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent we) {
				System.exit(0);
			}
		});
	}

	void open() {
		// Get all the capture devices
		devices = CaptureDeviceManager.getDeviceList(null);
		if (devices == null || devices.size() == 0) {
			System.err.println("Can't find any capture devices");
			System.exit(1);
		}
		
		// Remove all devices that don't capture video
		for (Iterator dev = devices.iterator(); dev.hasNext();) {
			cdi = (CaptureDeviceInfo) dev.next();
			Format[] formats = cdi.getFormats();
			boolean videoSupported = false;
			for (int i = 0; i < formats.length; i++) {
				if (formats[i] instanceof VideoFormat) {
					videoSupported = true;
					break;
				}
			}
			if (!videoSupported)
				dev.remove();
		}
		if (devices.size() == 0) {
			System.err.println("Can't find any video capture devices");
			System.exit(1);
		}
		
		// Create the menus
		MenuBar mb = new MenuBar();
		setMenuBar(mb);
		menuDevice = new Menu("Video device");
		mb.add(menuDevice);
		for (int i = 0; i < devices.size(); i++) {
			cdi = (CaptureDeviceInfo) devices.elementAt(i);
			MenuItem mi = new MenuItem(cdi.getName());
			mi.setActionCommand("D" + i);
			mi.addActionListener(this);
			menuDevice.add(mi);
		}
		menuFormat = new Menu("Video format");
		mb.add(menuFormat);

		// Start the first available capture device and format
		cdi = (CaptureDeviceInfo) devices.elementAt(0);
		updateVideoFormats();
		videoFormat = (VideoFormat) formats.elementAt(0);
		start();
		setVisible(true);
	}

	void start() {
		Dimension d = videoFormat.getSize();
		int w = (int) d.getWidth();
		int h = (int) d.getHeight();
		if (w < 300) {
			// less than 300 pixels wide is too small, double it
			w += w;
			h += h;
		}
		setSize(w, h + 70);
		try {
			captureDevice = (CaptureDevice)
				Manager.createDataSource(cdi.getLocator());
			captureDevice.getFormatControls()[0].setFormat(videoFormat);
			player = Manager.createPlayer((DataSource) captureDevice);
			player.addControllerListener(this);
			player.start();
		} catch (Exception e) {
			System.out.println(e.toString());
		}
	}

	void stop() {
		player.stop();
		player.close();
		captureDevice.disconnect();
		removeAll();
	}

	/**
	 * Counts number of 1 bits in a 32 bit unsigned number.
	 * 
	 * @param x - unsigned 32 bit number whose bits you wish to count.
	 * 
	 * @return number of 1 bits in x.
	 * @author Roedy Green
	 */
	public static int countBits(int x) {
		// collapsing partial parallel sums method
		// collapse 32x1 bit counts to 16x2 bit counts, mask 01010101
		x = (x >>> 1 & 0x55555555) + (x & 0x55555555);
		// collapse 16x2 bit counts to 8x4 bit counts, mask 00110011
		x = (x >>> 2 & 0x33333333) + (x & 0x33333333);
		// collapse 8x4 bit counts to 4x8 bit counts, mask 00001111
		x = (x >>> 4 & 0x0f0f0f0f) + (x & 0x0f0f0f0f);
		// collapse 4x8 bit counts to 2x16 bit counts
		x = (x >>> 8 & 0x00ff00ff) + (x & 0x00ff00ff);
		// collapse 2x16 bit counts to 1x32 bit count
		return (x >>> 16) + (x & 0x0000ffff);
	}

	/**
	 * @param t - YUV format type from YUVFormat.getYuvType()
	 * @return String name of the YUV format type
	 */
	String yuvName(int t) {
		if (t >= YUVFormat.YUV_SIGNED)
			t -= YUVFormat.YUV_SIGNED;
		switch (t) {
		case (YUVFormat.YUV_111):	return "YUV 1:1:1";
		case (YUVFormat.YUV_411):	return "YUV 4:1:1";
		case (YUVFormat.YUV_420):	return "YUV 4:2:0";
		case (YUVFormat.YUV_422):	return "YUV 4:2:2";
		case (YUVFormat.YUV_YUYV):	return "YUYV";
		case (YUVFormat.YUV_YVU9):	return "YVU9";
		default:					return "YUV ?";
		}
	}

	void updateVideoFormats() {
		// Get formats of the selected video capture device
		// and repopulate menuFormat
		menuFormat.removeAll();

		if (cdi != null) {
			Format[] f = cdi.getFormats();
			formats = new Vector();
			for (int i = 0; i < f.length; i++) {
				String fname = "Unknown format";
				Dimension d = ((VideoFormat) f[i]).getSize();
				int bpp = 0;
				if (f[i] instanceof RGBFormat) {
					fname = "RGB";
					RGBFormat rgbf = (RGBFormat) f[i];
					bpp = countBits(rgbf.getRedMask())
						+ countBits(rgbf.getGreenMask())
						+ countBits(rgbf.getBlueMask());
					if (bpp < 8)
						bpp = rgbf.getBitsPerPixel();
				} else if (f[i] instanceof YUVFormat) {
					fname = yuvName(((YUVFormat) f[i]).getYuvType());
				} else if (f[i] instanceof AviVideoFormat) {
					fname = "AVI";
					bpp = ((AviVideoFormat) f[i]).getBitsPerPixel();
				} else
					System.err.println("Unknown video format: "
						+ f[i].toString());
				String desc = fname + ", " + (int) d.getWidth() + "x"
					+ (int) d.getHeight();
				if (bpp > 0)
					desc += ", " + bpp + "bpp";
				MenuItem mi = new MenuItem(desc);
				mi.setActionCommand("F" + i);
				mi.addActionListener(this);
				menuFormat.add(mi);
				formats.addElement(f[i]);
			}
		}
	}

	public void actionPerformed(ActionEvent ae) {
		String cmd = ae.getActionCommand();
		int n = Integer.parseInt(cmd.substring(1));
		if (cmd.charAt(0) == 'D') {
			cdi = (CaptureDeviceInfo) devices.elementAt(n);
			updateVideoFormats();
		} else if (cmd.charAt(0) == 'F')
			videoFormat = (VideoFormat) formats.elementAt(n);
		stop();
		start();
	}

	public synchronized void controllerUpdate(ControllerEvent event) {
		if (event instanceof RealizeCompleteEvent) {
			Component comp;

			if ((comp = player.getVisualComponent()) != null)
				add(comp, "Center");
			if ((comp = player.getControlPanelComponent()) != null)
				add(comp, "South");
			validate();
		}
	}

	/**
	 * @param args - Command line arguments
	 */
	public static void main(String[] args) {
		VideoCapture myFrame = new VideoCapture("Video capture");
		myFrame.open();
	}

}
