Multicasting Images with Java
Monday, 8 September 2008 - Jochen Lüll

Distributing images within a network can be useful for a presentations or simply if a beamer is missing in a meeting room. This article shows how to multicast images and screenshots, so that they can be viewed by anyone within the network.
M
ulticasting is based on UDP
packets. UDP offers (unlike TCP) no
traffic
control. This means that packes are not guaranteed to be delivered in
the
right order or that they might not be delivered at all. Computers can
join a
so called Multicast Group which is represented by an IP-Number and a
port.Each member of the Multicast Group will receive the multicast packets belonging to that group.
Overview
In this sample application we will multicast screenshots and images so that they can be displayed everywhere within the network.The application consists of a sender and receiver Java application.
If you are not keen on knowing all the gory details, you can directly jump to the Running the Application section below and test the application.
The Basics
Receiving Multicast Packets
The receiving of multicast packets can be devided into the following steps:- In the first step the multicast socket ist created by calling MulticastSocket() which takes the port the socket is bound to as an argument.
- Next step is to join the Multicast Group.
A Multicast Group is represented by an IP Number ranging from 224.0.0.0 to 239.255.255.255. To join the group the joinGroup() method is called. - Create a byte buffer, a DatagramPacket object and call the MulticastSocket method receive() to accept a datagram packet.
- After receiving and processing the datagram
package by calling
getData()
on the
DatagramPacket the last step is leave the Multicast group and close the
multicast socket.
This is done by calling the methods leaveGroup() and close() on the multicast socket.
The following code illustrates the four steps.
/* Step one */
int port = 4444;
MulticastSocket ms = new MulticastSocket(port);
/* Step two */
String multicastAddress = "225.4.5.6";
InetAddress ia = InetAddress.getByName(multicastAddress);
ms.joinGroup(ia);
/* Step three */
DatagramPacket dp = new DatagramPacket(buffer, buffer.length);
ms.receive(dp);
byte[] data = dp.getData();
/* Step four */
ms.leaveGroup(ia);
ms.close();
int port = 4444;
MulticastSocket ms = new MulticastSocket(port);
/* Step two */
String multicastAddress = "225.4.5.6";
InetAddress ia = InetAddress.getByName(multicastAddress);
ms.joinGroup(ia);
/* Step three */
DatagramPacket dp = new DatagramPacket(buffer, buffer.length);
ms.receive(dp);
byte[] data = dp.getData();
/* Step four */
ms.leaveGroup(ia);
ms.close();
Receiving a Multicast Packet
Transmitting Multicast Packets
TTL
- Time To Live
Specifies the number of routers to pass (hopcount) before the packet is discarded.
Each router passing the packet will decrement the TTL by one. Packets with a TTL of zero will be discarded.
For MulticastSockets (in Java) the TTL defaults to 1, meaning all packets will stay within the same network.
The parameter can be altered by calling setTimeToLive() on a MulticastSocket
Transmitting multicast packets is a little bit easier than receiving
packets.Specifies the number of routers to pass (hopcount) before the packet is discarded.
Each router passing the packet will decrement the TTL by one. Packets with a TTL of zero will be discarded.
For MulticastSockets (in Java) the TTL defaults to 1, meaning all packets will stay within the same network.
The parameter can be altered by calling setTimeToLive() on a MulticastSocket
Time To Live (TTL)
A group has not to be joined if packets are just to be send.
These are the steps needed to transmit a multicast packet:
- In the first step an InetAddress for the specified Multicast Group IP Address is created.
- The next step is to construct a multicast socket and a dategram packet.
- The only thing now which is left is to send the datagram
over the
multicast socket and close the socket.
The four steps are shown below.
/* Step one */
String multicastAddress = "225.4.5.6";
InetAddress ia = InetAddress.getByName(multicastAddress);
/* Step two */
int port = 4444;
ms = new MulticastSocket();
DatagramPacket dp = new DatagramPacket(imageData, imageData.length,
ia, port);
/* Step three */
ms.send(dp);
ms.close();
String multicastAddress = "225.4.5.6";
InetAddress ia = InetAddress.getByName(multicastAddress);
/* Step two */
int port = 4444;
ms = new MulticastSocket();
DatagramPacket dp = new DatagramPacket(imageData, imageData.length,
ia, port);
/* Step three */
ms.send(dp);
ms.close();
Transmitting a Multicast Packet
Now that we we know how to transmit and reveice multicast packets we'll have a look how images are created.
Creating Screenshots
Sending screenshots over the network is fairly interesting because it gives us the possibility to show a presentation or the content of the screen to others.Creating screenshots in Java ist very easy. Since Java 1.3 the Robot class is available, which also gives us the possiblity to create screenshots.
The method createScreenCapture() of the Robot class takes a rectangle which specifies the portion of the screen to capture. In our case we'll always capture the whole screen.
Below is the source for the method getScreenshot() used to get screenshots within our program.
public static BufferedImage
getScreenshot() throws AWTException,
ImageFormatException, IOException {
Toolkit toolkit = Toolkit.getDefaultToolkit();
Dimension screenSize = toolkit.getScreenSize();
Rectangle screenRect = new Rectangle(screenSize);
Robot robot = new Robot();
BufferedImage image = robot.createScreenCapture(screenRect);
return image;
}
ImageFormatException, IOException {
Toolkit toolkit = Toolkit.getDefaultToolkit();
Dimension screenSize = toolkit.getScreenSize();
Rectangle screenRect = new Rectangle(screenSize);
Robot robot = new Robot();
BufferedImage image = robot.createScreenCapture(screenRect);
return image;
}
Creating a Screenshot
Reading Images from the Filesystem
In addintion to send screenshots the application can also send images which are stored in the filesystem.The application will read JPEG images from a directory in random order.
public
static BufferedImage getRandomImageFromDir(File dir) throws IOException
{
String[] images = dir.list(new ImageFileFilter());
int random = new Random().nextInt(images.length);
String fileName = dir.getAbsoluteFile() + File.separator + images[random];
File imageFile = new File(fileName);
return ImageIO.read(imageFile);
}
class ImageFileFilter implements FilenameFilter
{
public boolean accept( File dir, String name )
{
String nameLc = name.toLowerCase();
return nameLc.endsWith(".jpg") ? true : false;
}
}
String[] images = dir.list(new ImageFileFilter());
int random = new Random().nextInt(images.length);
String fileName = dir.getAbsoluteFile() + File.separator + images[random];
File imageFile = new File(fileName);
return ImageIO.read(imageFile);
}
class ImageFileFilter implements FilenameFilter
{
public boolean accept( File dir, String name )
{
String nameLc = name.toLowerCase();
return nameLc.endsWith(".jpg") ? true : false;
}
}
Reading Imges from the Filesystem
Transferring Data
Now that we know how to transfer multicast packets and how to obtain images, well have a closer look at how to transfer large images over the network.UDP Packet Sizes
UDP is (as well as TCP) encapsulated within a IP packet.The maximum IP packet size is 65535. From the maximum IP packet size we have to substract 20 bytes for the IP header and 8 bytes for the UDP header.
As a result the maximum datasize which can be transported within an UDP packet is 65507 bytes.
| 1 | 2 | 3 |
1 - IP Header (20 bytes) 2 - UDP Header (8 bytes) 3 - UDP Data (max 65507 bytes)
UDP Packet
The limit of 65507 bytes might be enought for small images, but for fullscreen screenshots it will not be sufficient.
It would be possible to scale down the images so that they fit into a single UDP packet but this would make these images hard to view.
In our solution we'll split up the images into appropriately sized chunks and transfer them over the network.
As mentioned above UDP has no traffic control build within, so we have to care for that ourselves.
Traffic Control
To make sure that the transmitted images slices are reassembled in the right order some headers are added to the UDP packet.This will reduce the amount of data which can be transferred within a packet, but that should be acceptable.
| Flags | 8 bit | Contains SESSION_END and SESSION_START flag |
| Session Number | 8 bit | Session the packet belongs to |
| Packets | 8 bit | Number of packets in total |
| Maximum
Packet Size |
16 bit | Maximum size of each packet |
| Packet Number | 8 bit | The number of the current package |
| Size | 16 bit | The data size of the current package |
According to the information given in the additional header information the image receiver can determine the final size of the image, the position of the current image data and weather the image is complete.
Image Sender and Receiver
The sender code snippet is shown below.
while(true) {
BufferedImage image = getScreenshot();
image = shrink(image, scalingFactor);
byte[] imageByteArray = bufferedImageToByteArray(image, OUTPUT_FORMAT);
int packets = (int) Math.ceil(imageByteArray.length / (float)DATAGRAM_MAX_SIZE);
int HEADER_SIZE = 8;
int MAX_PACKETS = 255;
int SESSION_START = 128;
int SESSION_END = 64;
if(packets > MAX_PACKETS) {
System.out.println("Image is too large to be transmitted!");
continue;
}
for(int i = 0; i <= packets; i++) {
int flags = 0;
flags = i == 0 ? flags | SESSION_START: flags;
flags = (i + 1) * DATAGRAM_MAX_SIZE > imageByteArray.length ? flags | SESSION_END : flags;
int size = (flags & SESSION_END) != SESSION_END ? DATAGRAM_MAX_SIZE : imageByteArray.length - i * DATAGRAM_MAX_SIZE;
byte[] data = new byte[HEADER_SIZE + size];
data[0] = (byte)flags;
data[1] = (byte)sessionNumber;
data[2] = (byte)packets;
data[3] = (byte)(DATAGRAM_MAX_SIZE >> 8);
data[4] = (byte)DATAGRAM_MAX_SIZE;
data[5] = (byte)i;
data[6] = (byte)(size >> 8);
data[7] = (byte)size;
System.arraycopy(imageByteArray, i * DATAGRAM_MAX_SIZE, data, HEADER_SIZE, size);
sender.sendImage(data, "225.4.5.6", 4444);
if((flags & SESSION_END) == SESSION_END) break;
}
Thread.sleep(SLEEP_MILLIS);
sessionNumber = sessionNumber < MAX_SESSION_NUMBER ? ++sessionNumber : 0;
}
BufferedImage image = getScreenshot();
image = shrink(image, scalingFactor);
byte[] imageByteArray = bufferedImageToByteArray(image, OUTPUT_FORMAT);
int packets = (int) Math.ceil(imageByteArray.length / (float)DATAGRAM_MAX_SIZE);
int HEADER_SIZE = 8;
int MAX_PACKETS = 255;
int SESSION_START = 128;
int SESSION_END = 64;
if(packets > MAX_PACKETS) {
System.out.println("Image is too large to be transmitted!");
continue;
}
for(int i = 0; i <= packets; i++) {
int flags = 0;
flags = i == 0 ? flags | SESSION_START: flags;
flags = (i + 1) * DATAGRAM_MAX_SIZE > imageByteArray.length ? flags | SESSION_END : flags;
int size = (flags & SESSION_END) != SESSION_END ? DATAGRAM_MAX_SIZE : imageByteArray.length - i * DATAGRAM_MAX_SIZE;
byte[] data = new byte[HEADER_SIZE + size];
data[0] = (byte)flags;
data[1] = (byte)sessionNumber;
data[2] = (byte)packets;
data[3] = (byte)(DATAGRAM_MAX_SIZE >> 8);
data[4] = (byte)DATAGRAM_MAX_SIZE;
data[5] = (byte)i;
data[6] = (byte)(size >> 8);
data[7] = (byte)size;
System.arraycopy(imageByteArray, i * DATAGRAM_MAX_SIZE, data, HEADER_SIZE, size);
sender.sendImage(data, "225.4.5.6", 4444);
if((flags & SESSION_END) == SESSION_END) break;
}
Thread.sleep(SLEEP_MILLIS);
sessionNumber = sessionNumber < MAX_SESSION_NUMBER ? ++sessionNumber : 0;
}
Image Sender
The receiver inspects the UDP data and determines weather the image belongs to the current session and if the image slice has already be stored.
After all slices habe been collected the image is displayed.
byte[] buffer = new
byte[DATAGRAM_MAX_SIZE];
while (true) {
DatagramPacket dp = new DatagramPacket(buffer, buffer.length);
ms.receive(dp);
byte[] data = dp.getData();
int SESSION_START = 128;
int SESSION_END = 64;
int HEADER_SIZE = 8;
short session = (short)(data[1] & 0xff);
short slices = (short)(data[2] & 0xff);
int maxPacketSize = (int)((data[3] & 0xff) << 8 | (data[4] & 0xff)); // mask the sign bit
short slice = (short)(data[5] & 0xff);
int size = (int)((data[6] & 0xff) << 8 | (data[7] & 0xff)); // mask the sign bit
if((data[0] & SESSION_START) == SESSION_START) {
if(session != currentSession) {
currentSession = session;
slicesStored = 0;
imageData = new byte[slices * maxPacketSize];
slicesCol = new int[slices];
sessionAvailable = true;
}
}
if(sessionAvailable && session == currentSession){
if(slicesCol != null && slicesCol[slice] == 0) {
slicesCol[slice] = 1;
System.arraycopy(data, HEADER_SIZE, imageData, slice * maxPacketSize, size);
slicesStored++;
}
}
if(slicesStored == slices) {
ByteArrayInputStream bis= new ByteArrayInputStream(imageData);
BufferedImage image = ImageIO.read(bis);
labelImage.setIcon(new ImageIcon(image));
windowImage.setIcon(new ImageIcon(image));
frame.pack();
}
}
while (true) {
DatagramPacket dp = new DatagramPacket(buffer, buffer.length);
ms.receive(dp);
byte[] data = dp.getData();
int SESSION_START = 128;
int SESSION_END = 64;
int HEADER_SIZE = 8;
short session = (short)(data[1] & 0xff);
short slices = (short)(data[2] & 0xff);
int maxPacketSize = (int)((data[3] & 0xff) << 8 | (data[4] & 0xff)); // mask the sign bit
short slice = (short)(data[5] & 0xff);
int size = (int)((data[6] & 0xff) << 8 | (data[7] & 0xff)); // mask the sign bit
if((data[0] & SESSION_START) == SESSION_START) {
if(session != currentSession) {
currentSession = session;
slicesStored = 0;
imageData = new byte[slices * maxPacketSize];
slicesCol = new int[slices];
sessionAvailable = true;
}
}
if(sessionAvailable && session == currentSession){
if(slicesCol != null && slicesCol[slice] == 0) {
slicesCol[slice] = 1;
System.arraycopy(data, HEADER_SIZE, imageData, slice * maxPacketSize, size);
slicesStored++;
}
}
if(slicesStored == slices) {
ByteArrayInputStream bis= new ByteArrayInputStream(imageData);
BufferedImage image = ImageIO.read(bis);
labelImage.setIcon(new ImageIcon(image));
windowImage.setIcon(new ImageIcon(image));
frame.pack();
}
}
Image Receiver
In principle that's all. Now let's have a look at the application.
The Application
The application works as described above but in addition to that has some additional features.These features are not explained within this article but should be easy to understand by reading the source code.
These additinal features are:
- Scaling of images by the sender
- Displaying the mouse position
- Displaying the image in fullscreen mode (receiver)
Running the Application

First download the application.
The ZIP file contains two jar files and the source code.
For a first test just double-click on the jar files (ImageSender.jar and ImageReceiver.jar).
This will launch the sender and the receiver application.
You should see something similar to the screenshots below.
Sender and Receiver
It doesn't make much sense to run both programs on one machine, but is a test if both applications are working.
For a real test, start the jar files on two different computers. The sender's screenshot should be displayed on the receiver's side.
If an images folder is available in the directory the sender is run, these images (JPEG format) will be sent instead of screenshots.
To view the images in fullscreen mode just press any key in the Multicast Image Receiver window.
Here are some sample screenshots.
Command Line Parameters
The programs can also be run by specifying command line paramters.When no command line paramters are specified the following default values are used:
IP Multicast Address:
225.4.5.6
Port: 4444
Display Mouse Cursor: 1
Update Interval in seconds (Sender): 2
Scaling Factor: 0.5
Port: 4444
Display Mouse Cursor: 1
Update Interval in seconds (Sender): 2
Scaling Factor: 0.5
The following command line parameters are available:
java -jar ImageSender.jar
scaling_factor
update_interval_sec show_mouse port ip_address
java -jar ImageReceiver.jar port ip_address
java -jar ImageReceiver.jar port ip_address
The command line options don't have to be specified completely, but none can be omitted in-between.
So specifying the following options is ok: java -jar ImageSender.jar 0.7 3 1 2048
Wireless Networks
Wireless networks can be a problem, because not all routers support multicasting to computers with wireless connection.The result might be that the images are not displayed in a timely fashion or that they are not displayed at all.
If you encounter problems with your wireless connection please try to connect your computer to the router directly via cable.
That's it. I hope you enjoyed the article.
In case of questions just drop me a line.
Happy coding!

(joschi)
Version: 0.2
Author's e-Mail address: jochen [at] fun2code.de




