/**
* Squidy Interaction Library is free software: you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* Squidy Interaction Library is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Squidy Interaction Library. If not, see
* <http://www.gnu.org/licenses/>.
*
* 2009 Human-Computer Interaction Group, University of Konstanz.
* <http://hci.uni-konstanz.de>
*
* Please contact info@squidy-lib.de or visit our website
* <http://www.squidy-lib.de> for further information.
*/
package org.squidy.nodes;
import java.awt.AWTException;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlType;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.squidy.manager.ProcessException;
import org.squidy.manager.controls.CheckBox;
import org.squidy.manager.controls.Slider;
import org.squidy.manager.controls.TextField;
import org.squidy.manager.data.IData;
import org.squidy.manager.data.Processor;
import org.squidy.manager.data.Property;
import org.squidy.manager.data.Throughput;
import org.squidy.manager.data.Processor.Status;
import org.squidy.manager.data.impl.DataObject;
import org.squidy.manager.data.impl.DataPosition2D;
import org.squidy.manager.model.AbstractNode;
/**
* <code>ScreenDispatcher</code>.
*
* <pre>
* Date: Sep 1, 2008
* Time: 7:03:00 PM
* </pre>
*
* @author Roman Rädle, <a
* href="mailto:Roman.Raedle@uni-konstanz.de">Roman.Raedle@uni-konstanz.de</a>, University
* of Konstanz
* @version $Id: ScreenDispatcher.java 772 2011-09-16 15:39:44Z raedle $
* @since 1.1.0
*/
@XmlType(name = "Screen Dispatcher")
@Processor(
name = "Screen Dispatcher",
icon = "/org/squidy/nodes/image/48x48/screen-dispatcher.png",
description = "/org/squidy/nodes/html/ScreenDispatcher.html",
types = { Processor.Type.OUTPUT },
tags = { "screen", "dispatcher", "capture", "casting", "video" },
status = Status.UNSTABLE
)
public class ScreenDispatcher extends AbstractNode {
// Logger to log info, error, debug,... messages.
private static final Log LOG = LogFactory.getLog(ScreenDispatcher.class);
// ################################################################################
// BEGIN OF ADJUSTABLES
// ################################################################################
@XmlAttribute(name = "remote-address")
@Property(
name = "Remote address",
description = "The address to the remote device."
)
@TextField
private String remoteAddress = "141.14.248.74";
/**
* @return the remoteAddress
*/
public final String getRemoteAddress() {
return remoteAddress;
}
/**
* @param remoteAddress
* the remoteAddress to set
*/
public final void setRemoteAddress(String remoteAddress) {
this.remoteAddress = remoteAddress;
if (isProcessing()) {
closeDispatcherSocket();
openDispatcherSocket();
}
}
@XmlAttribute(name = "remote-port")
@Property(
name = "Remote port",
description = "The port to the remote device."
)
@TextField
private int remotePort = 7777;
/**
* @return the remotePort
*/
public final int getRemotePort() {
return remotePort;
}
/**
* @param remotePort
* the remotePort to set
*/
public final void setRemotePort(int remotePort) {
this.remotePort = remotePort;
if (isProcessing()) {
closeDispatcherSocket();
openDispatcherSocket();
}
}
@XmlAttribute(name = "dispatch-position-x")
@Property(
name = "Dispatch position x",
description = "The X position of the dispatched screen."
)
@TextField
private int dispatchPositionX = 0;
/**
* @return the dispatchPositionX
*/
public final int getDispatchPositionX() {
return dispatchPositionX;
}
/**
* @param dispatchPositionX the dispatchPositionX to set
*/
public final void setDispatchPositionX(int dispatchPositionX) {
this.dispatchPositionX = dispatchPositionX;
}
@XmlAttribute(name = "dispatch-position-y")
@Property(
name = "Dispatch position y",
description = "The Y position of the dispatched screen."
)
@TextField
private int dispatchPositionY = 0;
/**
* @return the dispatchPositionY
*/
public final int getDispatchPositionY() {
return dispatchPositionY;
}
/**
* @param dispatchPositionY the dispatchPositionY to set
*/
public final void setDispatchPositionY(int dispatchPositionY) {
this.dispatchPositionY = dispatchPositionY;
}
@XmlAttribute(name = "dispatch-position-width")
@Property(
name = "Dispatch position width",
description = "The width of the dispatched screen."
)
@TextField
private int dispatchPositionWidth = 1280;
/**
* @return the dispatchPositionWidth
*/
public final int getDispatchPositionWidth() {
return dispatchPositionWidth;
}
/**
* @param dispatchPositionWidth the dispatchPositionWidth to set
*/
public final void setDispatchPositionWidth(int dispatchPositionWidth) {
this.dispatchPositionWidth = dispatchPositionWidth;
}
@XmlAttribute(name = "dispatch-position-height")
@Property(
name = "Dispatch position height",
description = "The height of the dispatched screen."
)
@TextField
private int dispatchPositionHeight = 800;
/**
* @return the dispatchPositionHeight
*/
public final int getDispatchPositionHeight() {
return dispatchPositionHeight;
}
/**
* @param dispatchPositionHeight the dispatchPositionHeight to set
*/
public final void setDispatchPositionHeight(int dispatchPositionHeight) {
this.dispatchPositionHeight = dispatchPositionHeight;
}
@XmlAttribute(name = "screen-width")
@Property(
name = "Screen width",
description = "The screen width of the screen."
)
@TextField
private int screenWidth = 1280;
/**
* @return the screenWidth
*/
public final int getScreenWidth() {
return screenWidth;
}
/**
* @param screenWidth the screenWidth to set
*/
public final void setScreenWidth(int screenWidth) {
this.screenWidth = screenWidth;
}
@XmlAttribute(name = "screen-height")
@Property(
name = "Screen height",
description = "The screen height of the screen."
)
@TextField
private int screenHeight = 800;
/**
* @return the screenHeight
*/
public final int getScreenHeight() {
return screenHeight;
}
/**
* @param screenHeight the screenHeight to set
*/
public final void setScreenHeight(int screenHeight) {
this.screenHeight = screenHeight;
}
@XmlAttribute(name = "remote-screen-width")
@Property(
name = "Remote screen width",
description = "The screen width of the remote screen."
)
@TextField
private int remoteScreenWidth = 320;
/**
* @return the remoteScreenWidth
*/
public final int getRemoteScreenWidth() {
return remoteScreenWidth;
}
/**
* @param remoteScreenWidth
* the remoteScreenWidth to set
*/
public final void setRemoteScreenWidth(int remoteScreenWidth) {
this.remoteScreenWidth = remoteScreenWidth;
}
@XmlAttribute(name = "remote-screen-height")
@Property(
name = "Remote screen height",
description = "The screen height of the remote screen."
)
@TextField
private int remoteScreenHeight = 480;
/**
* @return the remoteScreenHeight
*/
public final int getRemoteScreenHeight() {
return remoteScreenHeight;
}
/**
* @param remoteScreenHeight
* the remoteScreenHeight to set
*/
public final void setRemoteScreenHeight(int remoteScreenHeight) {
this.remoteScreenHeight = remoteScreenHeight;
}
@XmlAttribute(name = "scale-screen")
@Property(
name = "Scale screen",
description = "Whether the screen should be scaled before transmitting the screen to the remote device.."
)
@CheckBox
private boolean scaleScreen = true;
/**
* @return the scaleScreen
*/
public final boolean isScaleScreen() {
return scaleScreen;
}
/**
* @param scaleScreen the scaleScreen to set
*/
public final void setScaleScreen(boolean scaleScreen) {
this.scaleScreen = scaleScreen;
}
@XmlAttribute(name = "flip-screen")
@Property(
name = "Flip screen",
description = "Whether the screen ."
)
@CheckBox
private boolean flipScreen = false;
/**
* @return the flipScreen
*/
public final boolean isFlipScreen() {
return flipScreen;
}
/**
* @param flipScreen
* the flipScreen to set
*/
public final void setFlipScreen(boolean flipScreen) {
this.flipScreen = flipScreen;
}
@XmlAttribute(name = "refresh-rate")
@Property(
name = "Refresh rate",
description = "Indicates the refresh rate of the dispatcher.",
suffix = "ms"
)
@Slider(
minimumValue = 0,
maximumValue = 5000,
minorTicks = 500,
majorTicks = 2500,
showTicks = true,
showLabels = true
)
private int refreshRate = 500;
/**
* @return the refreshRate
*/
public final int getRefreshRate() {
return refreshRate;
}
/**
* @param refreshRate the refreshRate to set
*/
public final void setRefreshRate(int refreshRate) {
this.refreshRate = refreshRate;
}
@XmlAttribute(name = "screen-capture-quality")
@Property(
name = "Screen capture quality",
description = "Defines the quality a screen capture will be sent."
)
@Slider(
minimumValue = 0,
maximumValue = 100,
minorTicks = 10,
majorTicks = 25,
showTicks = true,
showLabels = true
)
private int screenCaptureQuality = 10;
/**
* @return the screenCaptureQuality
*/
public final int getScreenCaptureQuality() {
return screenCaptureQuality;
}
/**
* @param screenCaptureQuality the screenCaptureQuality to set
*/
public final void setScreenCaptureQuality(int screenCaptureQuality) {
this.screenCaptureQuality = screenCaptureQuality;
}
@XmlAttribute(name = "show-dispatching-debug-window")
@Property(
name = "Show dispatching debug window",
description = "Whether the dispatching debug window should be visible or not."
)
@CheckBox
private boolean showDispatchingDebugWindow = false;
/**
* @return the showDispatchingDebugWindow
*/
public final boolean isShowDispatchingDebugWindow() {
return showDispatchingDebugWindow;
}
/**
* @param showDispatchingDebugWindow
* the showDispatchingDebugWindow to set
*/
public final void setShowDispatchingDebugWindow(boolean showDispatchingDebugWindow) {
this.showDispatchingDebugWindow = showDispatchingDebugWindow;
if (isProcessing()) {
if (showDispatchingDebugWindow) {
showDispatchingDebugWindow();
}
else {
hideDispatchingDebugWindow();
}
}
}
// ################################################################################
// END OF ADJUSTABLES
// ################################################################################
private JFrame dispatchingDebugWindow;
private ScreenCaptureDebugging screenCaptureDebugging;
private Socket socket;
private CaptureScreen captureScreen;
/* (non-Javadoc)
* @see org.squidy.manager.ReflectionProcessable#onStart()
*/
@Override
public void onStart() throws ProcessException {
if (showDispatchingDebugWindow) {
showDispatchingDebugWindow();
}
createCaptureScreen(new Rectangle(dispatchPositionX, dispatchPositionY, dispatchPositionWidth, dispatchPositionHeight));
openDispatcherSocket();
new Thread() {
/* (non-Javadoc)
* @see java.lang.Thread#run()
*/
@Override
public void run() {
while (isProcessing()) {
try {
process2(null);
Thread.sleep(refreshRate);
}
catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}.start();
}
/* (non-Javadoc)
* @see org.squidy.manager.ReflectionProcessable#onStop()
*/
@Override
public void onStop() throws ProcessException {
closeDispatcherSocket();
hideDispatchingDebugWindow();
disposeCaptureScreen();
}
/**
* @param bounds
*/
private void createCaptureScreen(Rectangle bounds) throws ProcessException {
try {
captureScreen = new CaptureScreen(bounds);
}
catch (AWTException e) {
throw new ProcessException(e.getMessage(), e);
}
}
/**
*
*/
private void disposeCaptureScreen() {
if (captureScreen != null) {
captureScreen.dispose();
}
}
/**
*
*/
private void openDispatcherSocket() {
try {
socket = new Socket(remoteAddress, remotePort);
}
catch (UnknownHostException e) {
// throw new ProcessException("Could not initialize connection to screen capture receiver.", e);
}
catch (IOException e) {
// throw new ProcessException("Could not initialize connection to screen capture receiver.", e);
}
}
/**
*
*/
private void closeDispatcherSocket() {
if (socket != null) {
try {
socket.close();
}
catch (IOException e) {
throw new ProcessException("Could not close socket.", e);
}
socket = null;
}
}
/**
*
*/
private void reopenDispatcherSocket() {
closeDispatcherSocket();
openDispatcherSocket();
}
/**
*
*/
private void showDispatchingDebugWindow() {
if (dispatchingDebugWindow == null) {
dispatchingDebugWindow = new JFrame("Dispatching Debug Window");
dispatchingDebugWindow.setLayout(new BorderLayout());
dispatchingDebugWindow.setSize(new Dimension(320, 480));
dispatchingDebugWindow.setPreferredSize(new Dimension(320, 480));
screenCaptureDebugging = new ScreenCaptureDebugging();
dispatchingDebugWindow.add(screenCaptureDebugging, BorderLayout.CENTER);
dispatchingDebugWindow.setVisible(true);
}
}
/**
*
*/
private void hideDispatchingDebugWindow() {
if (dispatchingDebugWindow != null) {
dispatchingDebugWindow.setVisible(false);
dispatchingDebugWindow.dispose();
dispatchingDebugWindow = null;
}
}
/**
* @param dataPosition2D
* @return
*/
public IData process(DataPosition2D dataPosition2D) {
double x = dataPosition2D.getX();
double y = dataPosition2D.getY();
double scaleX = 1.0 / screenWidth;
double scaleY = 1.0 / screenHeight;
x = (scaleX * dispatchPositionX) + (x * scaleX * dispatchPositionWidth);
y = (scaleY * dispatchPositionY) + (y * scaleY * dispatchPositionHeight);
dataPosition2D.setX(x);
dataPosition2D.setY(y);
return dataPosition2D;
}
/**
* @param data
* @return
*/
public IData process(IData data) {
return data;
}
/**
* @param data
* @return
* @throws ProcessException
*/
public IData process2(IData data) throws ProcessException {
// TODO [RR]: This may causes an exception if squidy will be stopped but dispatching thread is still alive.
if (captureScreen == null) {
throw new ProcessException("Capture screen to capture a screenshot is null", data);
}
BufferedImage image = captureScreen.capture();
// Scale screen if set.
if (scaleScreen) {
image = scaleImageTo(image, remoteScreenWidth, remoteScreenHeight);
}
if (dispatchingDebugWindow != null) {
screenCaptureDebugging.setImage(image);
dispatchingDebugWindow.repaint();
}
// Reopen dispatcher socket if connection has been closed.
if (socket == null || socket.isClosed() || !socket.isConnected()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Connection is closed. Trying to reconnect in 5000 ms.");
}
try {
Thread.sleep(5000);
}
catch (InterruptedException e1) {
throw new ProcessException(e1.getMessage(), e1);
}
reopenDispatcherSocket();
}
else {
try {
float tmpScreenCaptureQuality = screenCaptureQuality / (float) 100;
// System.out.println("QUAL: " + tmpScreenCaptureQuality);
byte[] picture = ImageKit.getBytes(image, tmpScreenCaptureQuality);
DataOutputStream stream = new DataOutputStream(socket.getOutputStream());
// byte[] picture = pictureStream.toByteArray();
// int v = picture.length;
// System.out.println("LENGTH: " + v);
// System.out.println((v >>> 24) & 0xFF);
// System.out.println((v >>> 16) & 0xFF);
// System.out.println((v >>> 8) & 0xFF);
// System.out.println((v >>> 0) & 0xFF);
// System.out.println("SIZE: " + picture.length);
stream.writeInt(picture.length);
stream.write(picture);
}
catch (IOException e) {
closeDispatcherSocket();
if (LOG.isErrorEnabled()) {
LOG.error(e.getMessage(), e);
}
}
}
// else {
// if (LOG.isErrorEnabled()) {
// LOG.error("Dispatching socket is not open or have been closed already.");
// }
//// throw new ProcessException("Dispatching socket is not open or have been closed already.", data);
// }
return data;
}
/**
* Scales an image with pixel ratio awareness.
*
* @param image
* The source image that will be used to scale the image.
* @param width
* The maximum width of the scaled image.
* @param height
* The maximum height of the scaled image.
* @return Either the scaled image if its bigger than the source's width AND height or the
* source image.
*/
/**
* @param image
* @param width
* @param height
* @return
*/
private BufferedImage scaleImageTo(BufferedImage image, int width, int height) {
int imageWidth = image.getWidth(null);
int imageHeight = image.getHeight(null);
double scaleX = (double) width / imageWidth;
double scaleY = (double) height / imageHeight;
if (flipScreen) {
scaleX = (double) width / imageHeight;
scaleY = (double) height / imageWidth;
}
AffineTransform at = AffineTransform.getScaleInstance(scaleX, scaleY);
if (flipScreen) {
at.rotate(Math.PI / 2);
at.translate(0, -imageHeight);
}
BufferedImage scaledImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = scaledImage.createGraphics();
g2d.drawImage(image, at, null);
g2d.dispose();
return scaledImage;
}
@SuppressWarnings("serial")
private class ScreenCaptureDebugging extends JComponent {
private BufferedImage image;
public void setImage(BufferedImage image) {
if (this.image != null) {
}
this.image = image;
}
/*
* (non-Javadoc)
*
* @see javax.swing.JComponent#paintComponent(java.awt.Graphics)
*/
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (image != null) {
g.drawImage(image, 0, 0, null);
}
}
}
/**
* <code>CaptureScreen</code>.
*
* <pre>
* Date: Oct 29, 2008
* Time: 4:24:14 PM
* </pre>
*
* @author Roman Rädle, <a
* href="mailto:Roman.Raedle@uni-konstanz.de">Roman.Raedle@uni-konstanz.de</a>,
* University of Konstanz
* @version $Id: ScreenDispatcher.java 772 2011-09-16 15:39:44Z raedle $
* @since 1.1.0
*/
class CaptureScreen {
private CaptureData[] captureData;
private Rectangle bounds;
public CaptureScreen(Rectangle bounds) throws AWTException {
this.bounds = bounds;
initializeCaptureData(bounds);
}
/**
* @param bounds
* @throws AWTException
*/
private void initializeCaptureData(Rectangle bounds) throws AWTException {
List<CaptureData> data = new ArrayList<CaptureData>();
GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice[] graphicsDevices = graphicsEnvironment.getScreenDevices();
for (GraphicsDevice graphicsDevice : graphicsDevices) {
GraphicsConfiguration graphicsConfiguration = graphicsDevice.getDefaultConfiguration();
Rectangle screenBounds = graphicsConfiguration.getBounds();
if (screenBounds.intersects(bounds)) {
Rectangle intersectionBounds = screenBounds.intersection(bounds);
data.add(new CaptureData(new Robot(graphicsDevice), intersectionBounds));
}
}
captureData = data.toArray(new CaptureData[data.size()]);
}
/**
* @return
*/
public BufferedImage capture() {
if (captureData != null) {
BufferedImage image = new BufferedImage((int) bounds.getWidth(), (int) bounds.getHeight(), BufferedImage.TYPE_INT_RGB);
for (CaptureData data : captureData) {
data.captureToImage(image);
}
return image;
}
return null;
}
/**
*
*/
public void dispose() {
if (captureData != null) {
for (CaptureData data : captureData) {
data.dispose();
}
}
}
}
/**
* <code>CaptureScreen</code>.
*
* <pre>
* Date: Oct 29, 2008
* Time: 4:11:21 PM
* </pre>
*
* @author Roman Rädle, <a
* href="mailto:Roman.Raedle@uni-konstanz.de">Roman.Raedle@uni-konstanz.de</a>,
* University of Konstanz
* @version $Id: ScreenDispatcher.java 772 2011-09-16 15:39:44Z raedle $
* @since 1.1.0
*/
class CaptureData {
private Robot robot;
private Rectangle bounds;
/**
* @param robot
* @param bounds
*/
public CaptureData(Robot robot, Rectangle bounds) {
this.robot = robot;
this.bounds = bounds;
}
/**
* @param image
*/
public void captureToImage(BufferedImage image) {
if (robot == null) {
return;
}
// Rectangle adjustBounds = new Rectangle(0, 0, (int) bounds.getWidth(), (int) bounds.getHeight());
// BufferedImage screenCapture = robot.createScreenCapture(adjustBounds);
BufferedImage screenCapture = robot.createScreenCapture(bounds);
Graphics2D g2d = image.createGraphics();
g2d.drawImage(screenCapture, (int) bounds.getX() - dispatchPositionX, (int) bounds.getY() - dispatchPositionY, null);
g2d.dispose();
}
/**
*
*/
public void dispose() {
if (robot != null) {
robot = null;
}
}
}
/**
* <code>ImageKit</code>.
*
* <pre>
* Date: Oct 29, 2008
* Time: 4:11:25 PM
* </pre>
*
* @author Roman Rädle, <a
* href="mailto:Roman.Raedle@uni-konstanz.de">Roman.Raedle@uni-konstanz.de</a>,
* University of Konstanz
* @version $Id: ScreenDispatcher.java 772 2011-09-16 15:39:44Z raedle $
* @since 1.1.0
*/
static public class ImageKit {
//quality means jpeg output, if quality is < 0 ==> use default quality
public static void write(BufferedImage image, float quality, OutputStream out) throws IOException {
Iterator writers = ImageIO.getImageWritersBySuffix("jpeg");
if (!writers.hasNext())
throw new IllegalStateException("No writers found");
ImageWriter writer = (ImageWriter) writers.next();
ImageOutputStream ios = ImageIO.createImageOutputStream(out);
writer.setOutput(ios);
ImageWriteParam param = writer.getDefaultWriteParam();
if (quality >= 0) {
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(quality);
}
writer.write(null, new IIOImage(image, null, null), param);
}
public static BufferedImage read(byte[] bytes) {
try {
return ImageIO.read(new ByteArrayInputStream(bytes));
} catch(IOException e) {
throw new RuntimeException(e);
}
}
public static byte[] getBytes(BufferedImage image, float quality) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream(50000);
write(image, quality, out);
return out.toByteArray();
} catch(IOException e) {
throw new RuntimeException(e);
}
}
public static BufferedImage compress(BufferedImage image, float quality) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream(50000);
write(image, quality, out);
return ImageIO.read(new ByteArrayInputStream(out.toByteArray()));
} catch(IOException e) {
throw new RuntimeException(e);
}
}
}
/**
* @param args
*/
public static void main(String[] args) {
ScreenDispatcher sd = new ScreenDispatcher();
sd.setRemoteAddress("192.168.1.101");
sd.setScreenCaptureQuality(100);
sd.setDispatchPositionX(0);
sd.setDispatchPositionWidth(1440);
sd.setDispatchPositionHeight(900);
// sd.setFlipScreen(true);
sd.setRefreshRate(350);
sd.setShowDispatchingDebugWindow(true);
sd.start();
}
}