1
0
Fork 0
mirror of https://github.com/vanitasvitae/Smack.git synced 2024-12-26 12:37:58 +01:00

[SMACK-220] - New Jingle Based Screen Share API

git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/trunk@8144 b35dd754-fafc-0310-a699-88a17e54d16e
This commit is contained in:
Thiago Camargo 2007-05-02 19:55:59 +00:00 committed by thiago
parent 8e08a8ba4a
commit d3ce024c79
17 changed files with 1729 additions and 1 deletions

Binary file not shown.

View file

@ -0,0 +1,106 @@
/**
* $RCSfile$
* $Revision: $
* $Date: 25/12/2006
* <p/>
* Copyright 2003-2006 Jive Software.
* <p/>
* All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
* <p/>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.jingle.mediaimpl.sshare;
import org.jivesoftware.smackx.jingle.media.JingleMediaManager;
import org.jivesoftware.smackx.jingle.media.JingleMediaSession;
import org.jivesoftware.smackx.jingle.media.PayloadType;
import org.jivesoftware.smackx.jingle.mediaimpl.sshare.api.ImageEncoder;
import org.jivesoftware.smackx.jingle.mediaimpl.sshare.api.ImageDecoder;
import org.jivesoftware.smackx.jingle.nat.TransportCandidate;
import java.util.ArrayList;
import java.util.List;
/**
* Implements a JingleMediaManager for ScreenSharing.
* It currently uses an Audio payload Type. Which needs to be fixed in the next version.
*
* @author Thiago Camargo
*/
public class ScreenShareMediaManager extends JingleMediaManager {
private List<PayloadType> payloads = new ArrayList<PayloadType>();
private ImageDecoder decoder = null;
private ImageEncoder encoder = null;
public ScreenShareMediaManager() {
setupPayloads();
}
/**
* Setup API supported Payloads
*/
private void setupPayloads() {
payloads.add(new PayloadType.Audio(30, "sshare"));
}
/**
* Return all supported Payloads for this Manager.
*
* @return The Payload List
*/
public List<PayloadType> getPayloads() {
return payloads;
}
/**
* Returns a new JingleMediaSession
*
* @param payloadType payloadType
* @param remote remote Candidate
* @param local local Candidate
* @return JingleMediaSession JingleMediaSession
*/
public JingleMediaSession createMediaSession(PayloadType payloadType, final TransportCandidate remote, final TransportCandidate local) {
ScreenShareSession session = null;
session = new ScreenShareSession(payloadType, remote, local, "Screen");
if (encoder != null) {
session.setEncoder(encoder);
}
if (decoder != null) {
session.setDecoder(decoder);
}
return session;
}
public PayloadType getPreferredPayloadType() {
return super.getPreferredPayloadType();
}
public ImageDecoder getDecoder() {
return decoder;
}
public void setDecoder(ImageDecoder decoder) {
this.decoder = decoder;
}
public ImageEncoder getEncoder() {
return encoder;
}
public void setEncoder(ImageEncoder encoder) {
this.encoder = encoder;
}
}

View file

@ -0,0 +1,184 @@
/**
* $RCSfile$
* $Revision: $
* $Date: 08/11/2006
* <p/>
* Copyright 2003-2006 Jive Software.
* <p/>
* All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
* <p/>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.jingle.mediaimpl.sshare;
import org.jivesoftware.smackx.jingle.media.JingleMediaSession;
import org.jivesoftware.smackx.jingle.media.PayloadType;
import org.jivesoftware.smackx.jingle.mediaimpl.sshare.api.ImageDecoder;
import org.jivesoftware.smackx.jingle.mediaimpl.sshare.api.ImageEncoder;
import org.jivesoftware.smackx.jingle.mediaimpl.sshare.api.ImageReceiver;
import org.jivesoftware.smackx.jingle.mediaimpl.sshare.api.ImageTransmitter;
import org.jivesoftware.smackx.jingle.nat.TransportCandidate;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.UnknownHostException;
/**
* This Class implements a complete JingleMediaSession.
* It sould be used to transmit and receive captured images from the Display.
* This Class should be automaticly controlled by JingleSession.
* For better NAT Traversal support this implementation don´t support only receive or only transmit.
* To receive you MUST transmit. So the only implemented and functionally methods are startTransmit() and stopTransmit()
*
* @author Thiago Camargo
*/
public class ScreenShareSession extends JingleMediaSession {
private ImageTransmitter transmitter = null;
private ImageReceiver receiver = null;
/**
* Creates a org.jivesoftware.jingleaudio.jmf.AudioMediaSession with defined payload type, remote and local candidates
*
* @param payloadType Payload of the jmf
* @param remote the remote information. The candidate that the jmf will be sent to.
* @param local the local information. The candidate that will receive the jmf
* @param locator media locator
*/
public ScreenShareSession(final PayloadType payloadType, final TransportCandidate remote,
final TransportCandidate local, final String locator) {
super(payloadType, remote, local, "Screen");
initialize();
}
/**
* Initialize the Audio Channel to make it able to send and receive audio
*/
public void initialize() {
JFrame window = new JFrame();
JPanel jp = new JPanel();
window.add(jp);
window.setLocation(0, 0);
window.setSize(400, 400);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
try {
receiver = new ImageReceiver(InetAddress.getByName("0.0.0.0"), getRemote().getPort(), getLocal().getPort(), 800, 600);
System.out.println("Receiving on:" + receiver.getLocalPort());
}
catch (UnknownHostException e) {
e.printStackTrace();
}
jp.add(receiver);
receiver.setVisible(true);
window.setAlwaysOnTop(true);
window.setVisible(true);
try {
InetAddress remote = InetAddress.getByName(getRemote().getIp());
transmitter = new ImageTransmitter(receiver.getDatagramSocket(), remote, getRemote().getPort(), new Rectangle(0, 0, 800, 600));
}
catch (Exception e) {
}
}
/**
* Starts transmission and for NAT Traversal reasons start receiving also.
*/
public void startTrasmit() {
new Thread(transmitter).start();
}
/**
* Set transmit activity. If the active is true, the instance should trasmit.
* If it is set to false, the instance should pause transmit.
*
* @param active active state
*/
public void setTrasmit(boolean active) {
transmitter.setTransmit(true);
}
/**
* For NAT Reasons this method does nothing. Use startTransmit() to start transmit and receive jmf
*/
public void startReceive() {
// Do nothing
}
/**
* Stops transmission and for NAT Traversal reasons stop receiving also.
*/
public void stopTrasmit() {
}
/**
* For NAT Reasons this method does nothing. Use startTransmit() to start transmit and receive jmf
*/
public void stopReceive() {
// Do nothing
}
/**
* Obtain a free port we can use.
*
* @return A free port number.
*/
protected int getFreePort() {
ServerSocket ss;
int freePort = 0;
for (int i = 0; i < 10; i++) {
freePort = (int) (10000 + Math.round(Math.random() * 10000));
freePort = freePort % 2 == 0 ? freePort : freePort + 1;
try {
ss = new ServerSocket(freePort);
freePort = ss.getLocalPort();
ss.close();
return freePort;
}
catch (IOException e) {
e.printStackTrace();
}
}
try {
ss = new ServerSocket(0);
freePort = ss.getLocalPort();
ss.close();
}
catch (IOException e) {
e.printStackTrace();
}
return freePort;
}
public void setEncoder(ImageEncoder encoder) {
if (encoder != null) {
this.transmitter.setEncoder(encoder);
}
}
public void setDecoder(ImageDecoder decoder) {
if (decoder != null) {
this.receiver.setDecoder(decoder);
}
}
}

View file

@ -0,0 +1,98 @@
/*
Copyright 2006 Jerry Huxtable
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.jivesoftware.smackx.jingle.mediaimpl.sshare.api;
import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ColorModel;
/**
* A convenience class which implements those methods of BufferedImageOp which are rarely changed.
*/
public abstract class AbstractBufferedImageOp implements BufferedImageOp, Cloneable {
public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel dstCM) {
if ( dstCM == null )
dstCM = src.getColorModel();
return new BufferedImage(dstCM, dstCM.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), dstCM.isAlphaPremultiplied(), null);
}
public Rectangle2D getBounds2D( BufferedImage src ) {
return new Rectangle(0, 0, src.getWidth(), src.getHeight());
}
public Point2D getPoint2D( Point2D srcPt, Point2D dstPt ) {
if ( dstPt == null )
dstPt = new Point2D.Double();
dstPt.setLocation( srcPt.getX(), srcPt.getY() );
return dstPt;
}
public RenderingHints getRenderingHints() {
return null;
}
/**
* A convenience method for getting ARGB pixels from an image. This tries to avoid the performance
* penalty of BufferedImage.getRGB unmanaging the image.
* @param image a BufferedImage object
* @param x the left edge of the pixel block
* @param y the right edge of the pixel block
* @param width the width of the pixel arry
* @param height the height of the pixel arry
* @param pixels the array to hold the returned pixels. May be null.
* @return the pixels
* @see #setRGB
*/
public int[] getRGB( BufferedImage image, int x, int y, int width, int height, int[] pixels ) {
int type = image.getType();
if ( type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB )
return (int [])image.getRaster().getDataElements( x, y, width, height, pixels );
return image.getRGB( x, y, width, height, pixels, 0, width );
}
/**
* A convenience method for setting ARGB pixels in an image. This tries to avoid the performance
* penalty of BufferedImage.setRGB unmanaging the image.
* @param image a BufferedImage object
* @param x the left edge of the pixel block
* @param y the right edge of the pixel block
* @param width the width of the pixel arry
* @param height the height of the pixel arry
* @param pixels the array of pixels to set
* @see #getRGB
*/
public void setRGB( BufferedImage image, int x, int y, int width, int height, int[] pixels ) {
int type = image.getType();
if ( type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB )
image.getRaster().setDataElements( x, y, width, height, pixels );
else
image.setRGB( x, y, width, height, pixels, 0, width );
}
public Object clone() {
try {
return super.clone();
}
catch ( CloneNotSupportedException e ) {
return null;
}
}
}

View file

@ -0,0 +1,27 @@
package org.jivesoftware.smackx.jingle.mediaimpl.sshare.api;
import com.sixlegs.png.PngImage;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
/**
* Implements a default PNG Decoder
*/
public class DefaultDecoder implements ImageDecoder{
PngImage decoder = new PngImage();
public BufferedImage decode(ByteArrayInputStream stream) {
BufferedImage image = null;
try {
image = decoder.read(stream,true);
}
catch (IOException e) {
e.printStackTrace();
// Do nothing
}
return image;
}
}

View file

@ -0,0 +1,24 @@
package org.jivesoftware.smackx.jingle.mediaimpl.sshare.api;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* Implements a default PNG Encoder
*/
public class DefaultEncoder implements ImageEncoder{
public ByteArrayOutputStream encode(BufferedImage bufferedImage) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
ImageIO.write(bufferedImage, "png", baos);
}
catch (IOException e) {
e.printStackTrace();
baos = null;
}
return baos;
}
}

View file

@ -0,0 +1,13 @@
package org.jivesoftware.smackx.jingle.mediaimpl.sshare.api;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
/**
* Image Decoder Interface use this interface if you want to change the default decoder
*
* @author Thiago Rocha Camargo
*/
public interface ImageDecoder {
public BufferedImage decode(ByteArrayInputStream stream);
}

View file

@ -0,0 +1,13 @@
package org.jivesoftware.smackx.jingle.mediaimpl.sshare.api;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
/**
* Image Encoder Interface use this interface if you want to change the default encoder
*
* @author Thiago Rocha Camargo
*/
public interface ImageEncoder {
public ByteArrayOutputStream encode(BufferedImage bufferedImage);
}

View file

@ -0,0 +1,145 @@
package org.jivesoftware.smackx.jingle.mediaimpl.sshare.api;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
/**
* UDP Image Receiver.
* It uses PNG Tiles into UDP packets.
*
* @author Thiago Rocha Camargo
*/
public class ImageReceiver extends Canvas {
private boolean on = true;
private DatagramSocket socket;
private BufferedImage tiles[][];
private static final int tileWidth = ImageTransmitter.tileWidth;
private InetAddress localHost;
private InetAddress remoteHost;
private int localPort;
private int remotePort;
private ImageDecoder decoder;
public ImageReceiver(final InetAddress remoteHost, final int remotePort, final int localPort, int width, int height) {
tiles = new BufferedImage[width][height];
try {
socket = new DatagramSocket(localPort);
localHost = socket.getLocalAddress();
this.remoteHost = remoteHost;
this.remotePort = remotePort;
this.localPort = localPort;
this.decoder = new DefaultDecoder();
new Thread(new Runnable() {
public void run() {
byte buf[] = new byte[1024];
DatagramPacket p = new DatagramPacket(buf, 1024);
try {
while (on) {
socket.receive(p);
int length = p.getLength();
BufferedImage bufferedImage = decoder.decode(new ByteArrayInputStream(p.getData(), 0, length - 2));
if (bufferedImage != null) {
int x = p.getData()[length - 2];
int y = p.getData()[length - 1];
drawTile(x, y, bufferedImage);
}
}
}
catch (IOException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
public void run() {
byte buf[] = new byte[1024];
DatagramPacket p = new DatagramPacket(buf, 1024);
try {
while (on) {
p.setAddress(remoteHost);
p.setPort(remotePort);
socket.send(p);
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
catch (SocketException e) {
e.printStackTrace();
}
this.setSize(width, height);
}
public InetAddress getLocalHost() {
return localHost;
}
public InetAddress getRemoteHost() {
return remoteHost;
}
public int getLocalPort() {
return localPort;
}
public int getRemotePort() {
return remotePort;
}
public DatagramSocket getDatagramSocket() {
return socket;
}
public void drawTile(int x, int y, BufferedImage bufferedImage) {
tiles[x][y] = bufferedImage;
//repaint(x * tileWidth, y * tileWidth, tileWidth, tileWidth);
this.getGraphics().drawImage(bufferedImage, tileWidth * x, tileWidth * y, this);
}
public void paint(Graphics g) {
for (int i = 0; i < tiles.length; i++) {
for (int j = 0; j < tiles[0].length; j++) {
g.drawImage(tiles[i][j], tileWidth * i, tileWidth * j, this);
}
}
}
public ImageDecoder getDecoder() {
return decoder;
}
public void setDecoder(ImageDecoder decoder) {
this.decoder = decoder;
}
}

View file

@ -0,0 +1,239 @@
package org.jivesoftware.smackx.jingle.mediaimpl.sshare.api;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.PixelGrabber;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Arrays;
/**
* UDP Image Receiver.
* It uses PNG Tiles into UDP packets.
*
* @author Thiago Rocha Camargo
*/
public class ImageTransmitter implements Runnable {
private Robot robot;
private InetAddress localHost;
private InetAddress remoteHost;
private int localPort;
private int remotePort;
public static final int tileWidth = 25;
private boolean on = true;
private boolean transmit = false;
private DatagramSocket socket;
private Rectangle area;
private int tiles[][][];
private int maxI;
private int maxJ;
private boolean changed = false;
private final Object sync = new Object();
private ImageEncoder encoder;
public ImageTransmitter(DatagramSocket socket, InetAddress remoteHost, int remotePort, Rectangle area) {
try {
robot = new Robot();
maxI = (int) Math.ceil(area.getWidth() / tileWidth);
maxJ = (int) Math.ceil(area.getHeight() / tileWidth);
tiles = new int[maxI][maxJ][tileWidth * tileWidth];
this.area = area;
this.socket = socket;
localHost = socket.getLocalAddress();
localPort = socket.getLocalPort();
this.remoteHost = remoteHost;
this.remotePort = remotePort;
this.encoder = new DefaultEncoder();
transmit = true;
}
catch (AWTException e) {
e.printStackTrace();
}
}
public void start() {
byte buf[] = new byte[1024];
final DatagramPacket p = new DatagramPacket(buf, 1024);
/*
new Thread(
new Runnable() {
public void run() {
int w = (int) area.getWidth();
int h = (int) area.getHeight();
int tiles[][][] = new int[maxI][maxJ][tileWidth * tileWidth];
while (on) {
if (transmit) {
boolean differ = false;
BufferedImage capture = robot.createScreenCapture(area);
//ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
//ColorConvertOp op = new ColorConvertOp(cs, null);
//capture = op.filter(capture, null);
QuantizeFilter filter = new QuantizeFilter();
capture = filter.filter(capture, null);
long trace = System.currentTimeMillis();
for (int i = 0; i < maxI; i++) {
for (int j = 0; j < maxJ; j++) {
final BufferedImage bufferedImage = capture.getSubimage(i * tileWidth, j * tileWidth, tileWidth, tileWidth);
int pixels[] = new int[tileWidth * tileWidth];
PixelGrabber pg = new PixelGrabber(bufferedImage, 0, 0, tileWidth, tileWidth, pixels, 0, tileWidth);
try {
if (pg.grabPixels()) {
if (!differ) {
if (!Arrays.equals(tiles[i][j], pixels)) {
differ = true;
}
}
tiles[i][j] = pixels;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
if (differ) {
synchronized (sync) {
changed = true;
}
}
trace = (System.currentTimeMillis() - trace);
System.err.println("Loop Time:" + trace);
if (trace < 250) {
try {
Thread.sleep(250);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
).start();
*/
while (on) {
if (transmit) {
BufferedImage capture = robot.createScreenCapture(area);
//ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
//ColorConvertOp op = new ColorConvertOp(cs, null);
//capture = op.filter(capture, null);
QuantizeFilter filter = new QuantizeFilter();
capture = filter.filter(capture, null);
long trace = System.currentTimeMillis();
for (int i = 0; i < maxI; i++) {
for (int j = 0; j < maxJ; j++) {
final BufferedImage bufferedImage = capture.getSubimage(i * tileWidth, j * tileWidth, tileWidth, tileWidth);
int pixels[] = new int[tileWidth * tileWidth];
PixelGrabber pg = new PixelGrabber(bufferedImage, 0, 0, tileWidth, tileWidth, pixels, 0, tileWidth);
try {
if (pg.grabPixels()) {
if (!Arrays.equals(tiles[i][j], pixels)) {
ByteArrayOutputStream baos = encoder.encode(bufferedImage);
if (baos != null) {
Thread.sleep(1);
baos.write(i);
baos.write(j);
byte[] bytesOut = baos.toByteArray();
if (bytesOut.length > 400)
System.err.println(bytesOut.length);
p.setData(bytesOut);
p.setAddress(remoteHost);
p.setPort(remotePort);
try {
socket.send(p);
}
catch (IOException e) {
e.printStackTrace();
}
tiles[i][j] = pixels;
}
}
}
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
trace = (System.currentTimeMillis() - trace);
System.out.println("Loop Time:" + trace);
if (trace < 1000) {
try {
Thread.sleep(1000 - trace);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public void run() {
start();
}
public void setTransmit(boolean transmit) {
this.transmit = transmit;
}
public ImageEncoder getEncoder() {
return encoder;
}
public void setEncoder(ImageEncoder encoder) {
this.encoder = encoder;
}
}

View file

@ -0,0 +1,282 @@
/*
Copyright 2006 Jerry Huxtable
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.jivesoftware.smackx.jingle.mediaimpl.sshare.api;
import java.io.PrintStream;
import java.util.Vector;
/**
* An image Quantizer based on the Octree algorithm. This is a very basic implementation
* at present and could be much improved by picking the nodes to reduce more carefully
* (i.e. not completely at random) when I get the time.
*/
public class OctTreeQuantizer implements Quantizer {
/**
* The greatest depth the tree is allowed to reach
*/
final static int MAX_LEVEL = 5;
/**
* An Octtree node.
*/
class OctTreeNode {
int children;
int level;
OctTreeNode parent;
OctTreeNode leaf[] = new OctTreeNode[8];
boolean isLeaf;
int count;
int totalRed;
int totalGreen;
int totalBlue;
int index;
/**
* A debugging method which prints the tree out.
*/
public void list(PrintStream s, int level) {
for (int i = 0; i < level; i++)
System.out.print(' ');
if (count == 0)
System.out.println(index+": count="+count);
else
System.out.println(index+": count="+count+" red="+(totalRed/count)+" green="+(totalGreen/count)+" blue="+(totalBlue/count));
for (int i = 0; i < 8; i++)
if (leaf[i] != null)
leaf[i].list(s, level+2);
}
}
private int nodes = 0;
private OctTreeNode root;
private int reduceColors;
private int maximumColors;
private int colors = 0;
private Vector[] colorList;
public OctTreeQuantizer() {
setup(256);
colorList = new Vector[MAX_LEVEL+1];
for (int i = 0; i < MAX_LEVEL+1; i++)
colorList[i] = new Vector();
root = new OctTreeNode();
}
/**
* Initialize the quantizer. This should be called before adding any pixels.
* @param numColors the number of colors we're quantizing to.
*/
public void setup(int numColors) {
maximumColors = numColors;
reduceColors = Math.max(512, numColors * 2);
}
/**
* Add pixels to the quantizer.
* @param pixels the array of ARGB pixels
* @param offset the offset into the array
* @param count the count of pixels
*/
public void addPixels(int[] pixels, int offset, int count) {
for (int i = 0; i < count; i++) {
insertColor(pixels[i+offset]);
if (colors > reduceColors)
reduceTree(reduceColors);
}
}
/**
* Get the color table index for a color.
* @param rgb the color
* @return the index
*/
public int getIndexForColor(int rgb) {
int red = (rgb >> 16) & 0xff;
int green = (rgb >> 8) & 0xff;
int blue = rgb & 0xff;
OctTreeNode node = root;
for (int level = 0; level <= MAX_LEVEL; level++) {
OctTreeNode child;
int bit = 0x80 >> level;
int index = 0;
if ((red & bit) != 0)
index += 4;
if ((green & bit) != 0)
index += 2;
if ((blue & bit) != 0)
index += 1;
child = node.leaf[index];
if (child == null)
return node.index;
else if (child.isLeaf)
return child.index;
else
node = child;
}
System.out.println("getIndexForColor failed");
return 0;
}
private void insertColor(int rgb) {
int red = (rgb >> 16) & 0xff;
int green = (rgb >> 8) & 0xff;
int blue = rgb & 0xff;
OctTreeNode node = root;
// System.out.println("insertColor="+Integer.toHexString(rgb));
for (int level = 0; level <= MAX_LEVEL; level++) {
OctTreeNode child;
int bit = 0x80 >> level;
int index = 0;
if ((red & bit) != 0)
index += 4;
if ((green & bit) != 0)
index += 2;
if ((blue & bit) != 0)
index += 1;
child = node.leaf[index];
if (child == null) {
node.children++;
child = new OctTreeNode();
child.parent = node;
node.leaf[index] = child;
node.isLeaf = false;
nodes++;
colorList[level].addElement(child);
if (level == MAX_LEVEL) {
child.isLeaf = true;
child.count = 1;
child.totalRed = red;
child.totalGreen = green;
child.totalBlue = blue;
child.level = level;
colors++;
return;
}
node = child;
} else if (child.isLeaf) {
child.count++;
child.totalRed += red;
child.totalGreen += green;
child.totalBlue += blue;
return;
} else
node = child;
}
System.out.println("insertColor failed");
}
private void reduceTree(int numColors) {
for (int level = MAX_LEVEL-1; level >= 0; level--) {
Vector v = colorList[level];
if (v != null && v.size() > 0) {
for (int j = 0; j < v.size(); j++) {
OctTreeNode node = (OctTreeNode)v.elementAt(j);
if (node.children > 0) {
for (int i = 0; i < 8; i++) {
OctTreeNode child = node.leaf[i];
if (child != null) {
if (!child.isLeaf)
System.out.println("not a leaf!");
node.count += child.count;
node.totalRed += child.totalRed;
node.totalGreen += child.totalGreen;
node.totalBlue += child.totalBlue;
node.leaf[i] = null;
node.children--;
colors--;
nodes--;
colorList[level+1].removeElement(child);
}
}
node.isLeaf = true;
colors++;
if (colors <= numColors)
return;
}
}
}
}
System.out.println("Unable to reduce the OctTree");
}
/**
* Build the color table.
* @return the color table
*/
public int[] buildColorTable() {
int[] table = new int[colors];
buildColorTable(root, table, 0);
return table;
}
/**
* A quick way to use the quantizer. Just create a table the right size and pass in the pixels.
* @param inPixels the input colors
* @param table the output color table
*/
public void buildColorTable(int[] inPixels, int[] table) {
int count = inPixels.length;
maximumColors = table.length;
for (int i = 0; i < count; i++) {
insertColor(inPixels[i]);
if (colors > reduceColors)
reduceTree(reduceColors);
}
if (colors > maximumColors)
reduceTree(maximumColors);
buildColorTable(root, table, 0);
}
private int buildColorTable(OctTreeNode node, int[] table, int index) {
if (colors > maximumColors)
reduceTree(maximumColors);
if (node.isLeaf) {
int count = node.count;
table[index] = 0xff000000 |
((node.totalRed/count) << 16) |
((node.totalGreen/count) << 8) |
node.totalBlue/count;
node.index = index++;
} else {
for (int i = 0; i < 8; i++) {
if (node.leaf[i] != null) {
node.index = index;
index = buildColorTable(node.leaf[i], table, index);
}
}
}
return index;
}
}

View file

@ -0,0 +1,223 @@
/*
Copyright 2006 Jerry Huxtable
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.jivesoftware.smackx.jingle.mediaimpl.sshare.api;
import java.awt.*;
import java.util.Random;
/**
* Some more useful math functions for image processing.
* These are becoming obsolete as we move to Java2D. Use MiscComposite instead.
*/
public class PixelUtils {
public final static int REPLACE = 0;
public final static int NORMAL = 1;
public final static int MIN = 2;
public final static int MAX = 3;
public final static int ADD = 4;
public final static int SUBTRACT = 5;
public final static int DIFFERENCE = 6;
public final static int MULTIPLY = 7;
public final static int HUE = 8;
public final static int SATURATION = 9;
public final static int VALUE = 10;
public final static int COLOR = 11;
public final static int SCREEN = 12;
public final static int AVERAGE = 13;
public final static int OVERLAY = 14;
public final static int CLEAR = 15;
public final static int EXCHANGE = 16;
public final static int DISSOLVE = 17;
public final static int DST_IN = 18;
public final static int ALPHA = 19;
public final static int ALPHA_TO_GRAY = 20;
private static Random randomGenerator = new Random();
/**
* Clamp a value to the range 0..255
*/
public static int clamp(int c) {
if (c < 0)
return 0;
if (c > 255)
return 255;
return c;
}
public static int interpolate(int v1, int v2, float f) {
return clamp((int)(v1+f*(v2-v1)));
}
public static int brightness(int rgb) {
int r = (rgb >> 16) & 0xff;
int g = (rgb >> 8) & 0xff;
int b = rgb & 0xff;
return (r+g+b)/3;
}
public static boolean nearColors(int rgb1, int rgb2, int tolerance) {
int r1 = (rgb1 >> 16) & 0xff;
int g1 = (rgb1 >> 8) & 0xff;
int b1 = rgb1 & 0xff;
int r2 = (rgb2 >> 16) & 0xff;
int g2 = (rgb2 >> 8) & 0xff;
int b2 = rgb2 & 0xff;
return Math.abs(r1-r2) <= tolerance && Math.abs(g1-g2) <= tolerance && Math.abs(b1-b2) <= tolerance;
}
private final static float hsb1[] = new float[3];//FIXME-not thread safe
private final static float hsb2[] = new float[3];//FIXME-not thread safe
// Return rgb1 painted onto rgb2
public static int combinePixels(int rgb1, int rgb2, int op) {
return combinePixels(rgb1, rgb2, op, 0xff);
}
public static int combinePixels(int rgb1, int rgb2, int op, int extraAlpha, int channelMask) {
return (rgb2 & ~channelMask) | combinePixels(rgb1 & channelMask, rgb2, op, extraAlpha);
}
public static int combinePixels(int rgb1, int rgb2, int op, int extraAlpha) {
if (op == REPLACE)
return rgb1;
int a1 = (rgb1 >> 24) & 0xff;
int r1 = (rgb1 >> 16) & 0xff;
int g1 = (rgb1 >> 8) & 0xff;
int b1 = rgb1 & 0xff;
int a2 = (rgb2 >> 24) & 0xff;
int r2 = (rgb2 >> 16) & 0xff;
int g2 = (rgb2 >> 8) & 0xff;
int b2 = rgb2 & 0xff;
switch (op) {
case NORMAL:
break;
case MIN:
r1 = Math.min(r1, r2);
g1 = Math.min(g1, g2);
b1 = Math.min(b1, b2);
break;
case MAX:
r1 = Math.max(r1, r2);
g1 = Math.max(g1, g2);
b1 = Math.max(b1, b2);
break;
case ADD:
r1 = clamp(r1+r2);
g1 = clamp(g1+g2);
b1 = clamp(b1+b2);
break;
case SUBTRACT:
r1 = clamp(r2-r1);
g1 = clamp(g2-g1);
b1 = clamp(b2-b1);
break;
case DIFFERENCE:
r1 = clamp(Math.abs(r1-r2));
g1 = clamp(Math.abs(g1-g2));
b1 = clamp(Math.abs(b1-b2));
break;
case MULTIPLY:
r1 = clamp(r1*r2/255);
g1 = clamp(g1*g2/255);
b1 = clamp(b1*b2/255);
break;
case DISSOLVE:
if ((randomGenerator.nextInt() & 0xff) <= a1) {
r1 = r2;
g1 = g2;
b1 = b2;
}
break;
case AVERAGE:
r1 = (r1+r2)/2;
g1 = (g1+g2)/2;
b1 = (b1+b2)/2;
break;
case HUE:
case SATURATION:
case VALUE:
case COLOR:
Color.RGBtoHSB(r1, g1, b1, hsb1);
Color.RGBtoHSB(r2, g2, b2, hsb2);
switch (op) {
case HUE:
hsb2[0] = hsb1[0];
break;
case SATURATION:
hsb2[1] = hsb1[1];
break;
case VALUE:
hsb2[2] = hsb1[2];
break;
case COLOR:
hsb2[0] = hsb1[0];
hsb2[1] = hsb1[1];
break;
}
rgb1 = Color.HSBtoRGB(hsb2[0], hsb2[1], hsb2[2]);
r1 = (rgb1 >> 16) & 0xff;
g1 = (rgb1 >> 8) & 0xff;
b1 = rgb1 & 0xff;
break;
case SCREEN:
r1 = 255 - ((255 - r1) * (255 - r2)) / 255;
g1 = 255 - ((255 - g1) * (255 - g2)) / 255;
b1 = 255 - ((255 - b1) * (255 - b2)) / 255;
break;
case OVERLAY:
int m, s;
s = 255 - ((255 - r1) * (255 - r2)) / 255;
m = r1 * r2 / 255;
r1 = (s * r1 + m * (255 - r1)) / 255;
s = 255 - ((255 - g1) * (255 - g2)) / 255;
m = g1 * g2 / 255;
g1 = (s * g1 + m * (255 - g1)) / 255;
s = 255 - ((255 - b1) * (255 - b2)) / 255;
m = b1 * b2 / 255;
b1 = (s * b1 + m * (255 - b1)) / 255;
break;
case CLEAR:
r1 = g1 = b1 = 0xff;
break;
case DST_IN:
r1 = clamp((r2*a1)/255);
g1 = clamp((g2*a1)/255);
b1 = clamp((b2*a1)/255);
a1 = clamp((a2*a1)/255);
return (a1 << 24) | (r1 << 16) | (g1 << 8) | b1;
case ALPHA:
a1 = a1*a2/255;
return (a1 << 24) | (r2 << 16) | (g2 << 8) | b2;
case ALPHA_TO_GRAY:
int na = 255-a1;
return (a1 << 24) | (na << 16) | (na << 8) | na;
}
if (extraAlpha != 0xff || a1 != 0xff) {
a1 = a1*extraAlpha/255;
int a3 = (255-a1)*a2/255;
r1 = clamp((r1*a1+r2*a3)/255);
g1 = clamp((g1*a1+g2*a3)/255);
b1 = clamp((b1*a1+b2*a3)/255);
a1 = clamp(a1+a3);
}
return (a1 << 24) | (r1 << 16) | (g1 << 8) | b1;
}
}

View file

@ -0,0 +1,178 @@
/*
Copyright 2006 Jerry Huxtable
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.jivesoftware.smackx.jingle.mediaimpl.sshare.api;
import java.awt.*;
/**
* A filter which quantizes an image to a set number of colors - useful for producing
* images which are to be encoded using an index color model. The filter can perform
* Floyd-Steinberg error-diffusion dithering if required. At present, the quantization
* is done using an octtree algorithm but I eventually hope to add more quantization
* methods such as median cut. Note: at present, the filter produces an image which
* uses the RGB color model (because the application it was written for required it).
* I hope to extend it to produce an IndexColorModel by request.
*/
public class QuantizeFilter extends WholeImageFilter {
/**
* Floyd-Steinberg dithering matrix.
*/
protected final static int[] matrix = {
0, 0, 0,
0, 0, 7,
3, 5, 1,
};
private int sum = 3+5+7+1;
private boolean dither;
private int numColors = 256;
private boolean serpentine = true;
/**
* Set the number of colors to quantize to.
* @param numColors the number of colors. The default is 256.
*/
public void setNumColors(int numColors) {
this.numColors = Math.min(Math.max(numColors, 8), 256);
}
/**
* Get the number of colors to quantize to.
* @return the number of colors.
*/
public int getNumColors() {
return numColors;
}
/**
* Set whether to use dithering or not. If not, the image is posterized.
* @param dither true to use dithering
*/
public void setDither(boolean dither) {
this.dither = dither;
}
/**
* Return the dithering setting
* @return the current setting
*/
public boolean getDither() {
return dither;
}
/**
* Set whether to use a serpentine pattern for return or not. This can reduce 'avalanche' artifacts in the output.
* @param serpentine true to use serpentine pattern
*/
public void setSerpentine(boolean serpentine) {
this.serpentine = serpentine;
}
/**
* Return the serpentine setting
* @return the current setting
*/
public boolean getSerpentine() {
return serpentine;
}
public void quantize(int[] inPixels, int[] outPixels, int width, int height, int numColors, boolean dither, boolean serpentine) {
int count = width*height;
Quantizer quantizer = new OctTreeQuantizer();
quantizer.setup(numColors);
quantizer.addPixels(inPixels, 0, count);
int[] table = quantizer.buildColorTable();
if (!dither) {
for (int i = 0; i < count; i++)
outPixels[i] = table[quantizer.getIndexForColor(inPixels[i])];
} else {
int index = 0;
for (int y = 0; y < height; y++) {
boolean reverse = serpentine && (y & 1) == 1;
int direction;
if (reverse) {
index = y*width+width-1;
direction = -1;
} else {
index = y*width;
direction = 1;
}
for (int x = 0; x < width; x++) {
int rgb1 = inPixels[index];
int rgb2 = table[quantizer.getIndexForColor(rgb1)];
outPixels[index] = rgb2;
int r1 = (rgb1 >> 16) & 0xff;
int g1 = (rgb1 >> 8) & 0xff;
int b1 = rgb1 & 0xff;
int r2 = (rgb2 >> 16) & 0xff;
int g2 = (rgb2 >> 8) & 0xff;
int b2 = rgb2 & 0xff;
int er = r1-r2;
int eg = g1-g2;
int eb = b1-b2;
for (int i = -1; i <= 1; i++) {
int iy = i+y;
if (0 <= iy && iy < height) {
for (int j = -1; j <= 1; j++) {
int jx = j+x;
if (0 <= jx && jx < width) {
int w;
if (reverse)
w = matrix[(i+1)*3-j+1];
else
w = matrix[(i+1)*3+j+1];
if (w != 0) {
int k = reverse ? index - j : index + j;
rgb1 = inPixels[k];
r1 = (rgb1 >> 16) & 0xff;
g1 = (rgb1 >> 8) & 0xff;
b1 = rgb1 & 0xff;
r1 += er * w/sum;
g1 += eg * w/sum;
b1 += eb * w/sum;
inPixels[k] = (PixelUtils.clamp(r1) << 16) | (PixelUtils.clamp(g1) << 8) | PixelUtils.clamp(b1);
}
}
}
}
}
index += direction;
}
}
}
}
protected int[] filterPixels( int width, int height, int[] inPixels, Rectangle transformedSpace ) {
int[] outPixels = new int[width*height];
quantize(inPixels, outPixels, width, height, numColors, dither, serpentine);
return outPixels;
}
public String toString() {
return "Colors/Quantize...";
}
}

View file

@ -0,0 +1,53 @@
/*
Copyright 2006 Jerry Huxtable
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.jivesoftware.smackx.jingle.mediaimpl.sshare.api;
/**
* The interface for an image quantizer. The addColor method is called (repeatedly
* if necessary) with all the image pixels. A color table can then be returned by
* calling the buildColorTable method.
*/
public interface Quantizer {
/**
* Initialize the quantizer. This should be called before adding any pixels.
* @param numColors the number of colors we're quantizing to.
*/
public void setup(int numColors);
/**
* Add pixels to the quantizer.
* @param pixels the array of ARGB pixels
* @param offset the offset into the array
* @param count the count of pixels
*/
public void addPixels(int[] pixels, int offset, int count);
/**
* Build a color table from the added pixels.
* @return an array of ARGB pixels representing a color table
*/
public int[] buildColorTable();
/**
* Using the previously-built color table, return the index into that table for a pixel.
* This is guaranteed to return a valid index - returning the index of a color closer
* to that requested if necessary.
* @param rgb the pixel to find
* @return the pixel's index in the color table
*/
public int getIndexForColor(int rgb);
}

View file

@ -0,0 +1,86 @@
/*
Copyright 2006 Jerry Huxtable
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.jivesoftware.smackx.jingle.mediaimpl.sshare.api;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.WritableRaster;
/**
* A filter which acts as a superclass for filters which need to have the whole image in memory
* to do their stuff.
*/
public abstract class WholeImageFilter extends AbstractBufferedImageOp {
/**
* The output image bounds.
*/
protected Rectangle transformedSpace;
/**
* The input image bounds.
*/
protected Rectangle originalSpace;
/**
* Construct a WholeImageFilter.
*/
public WholeImageFilter() {
}
public BufferedImage filter( BufferedImage src, BufferedImage dst ) {
int width = src.getWidth();
int height = src.getHeight();
int type = src.getType();
WritableRaster srcRaster = src.getRaster();
originalSpace = new Rectangle(0, 0, width, height);
transformedSpace = new Rectangle(0, 0, width, height);
transformSpace(transformedSpace);
if ( dst == null ) {
ColorModel dstCM = src.getColorModel();
dst = new BufferedImage(dstCM, dstCM.createCompatibleWritableRaster(transformedSpace.width, transformedSpace.height), dstCM.isAlphaPremultiplied(), null);
}
WritableRaster dstRaster = dst.getRaster();
int[] inPixels = getRGB( src, 0, 0, width, height, null );
inPixels = filterPixels( width, height, inPixels, transformedSpace );
setRGB( dst, 0, 0, transformedSpace.width, transformedSpace.height, inPixels );
return dst;
}
/**
* Calculate output bounds for given input bounds.
* @param rect input and output rectangle
*/
protected void transformSpace(Rectangle rect) {
}
/**
* Actually filter the pixels.
* @param width the image width
* @param height the image height
* @param inPixels the image pixels
* @param transformedSpace the output bounds
* @return the output pixels
*/
protected abstract int[] filterPixels( int width, int height, int[] inPixels, Rectangle transformedSpace );
}

View file

@ -909,7 +909,8 @@ public class JingleManagerTest extends SmackTestCase {
System.out.println(valCounter());
assertTrue(valCounter() == 8);
//Thread.sleep(15000);
Thread.sleep(15000);
}
catch (Exception e) {

View file

@ -26,6 +26,7 @@ import org.jivesoftware.smackx.jingle.mediaimpl.jmf.JmfMediaManager;
import org.jivesoftware.smackx.jingle.mediaimpl.jmf.AudioChannel;
import org.jivesoftware.smackx.jingle.mediaimpl.jspeex.SpeexMediaManager;
import org.jivesoftware.smackx.jingle.mediaimpl.multi.MultiMediaManager;
import org.jivesoftware.smackx.jingle.mediaimpl.sshare.ScreenShareMediaManager;
import org.jivesoftware.smackx.jingle.listeners.JingleSessionRequestListener;
import org.jivesoftware.smackx.jingle.listeners.JingleSessionStateListener;
import org.jivesoftware.smackx.jingle.media.JingleMediaManager;
@ -270,6 +271,61 @@ public class JingleMediaTest extends SmackTestCase {
e.printStackTrace();
}
}
public void testCompleteScreenShare() {
try {
//XMPPConnection.DEBUG_ENABLED = true;
XMPPConnection x0 = getConnection(0);
XMPPConnection x1 = getConnection(1);
final JingleManager jm0 = new JingleManager(
x0, new ICETransportManager(x0,"stun.xten.net",3478));
final JingleManager jm1 = new JingleManager(
x1, new ICETransportManager(x1,"stun.xten.net",3478));
JingleMediaManager jingleMediaManager0 = new ScreenShareMediaManager();
JingleMediaManager jingleMediaManager1 = new ScreenShareMediaManager();
jm0.setMediaManager(jingleMediaManager0);
jm1.setMediaManager(jingleMediaManager1);
jm1.addJingleSessionRequestListener(new JingleSessionRequestListener() {
public void sessionRequested(final JingleSessionRequest request) {
try {
IncomingJingleSession session = request.accept(jm1.getMediaManager().getPayloads());
session.start(request);
}
catch (XMPPException e) {
e.printStackTrace();
}
}
});
OutgoingJingleSession js0 = jm0.createOutgoingJingleSession(x1.getUser());
js0.start();
Thread.sleep(150000);
js0.terminate();
Thread.sleep(6000);
x0.disconnect();
x1.disconnect();
}
catch (Exception e) {
e.printStackTrace();
}
}
public void testCompleteWithBridge() {