Multicasting Images with Java

Monday, 8 September 2008 - Jochen Lüll
Screenshot Compiz
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:
  1. In the first step the multicast socket ist created by calling MulticastSocket() which takes the port the socket is bound to as an argument.
  2. 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.
  3. Create a byte buffer, a DatagramPacket object and call the MulticastSocket method receive() to accept a datagram packet.
  4. 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();
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


Time To Live (TTL)
Transmitting multicast packets is a little bit easier than receiving packets.
A group has not to be joined if packets are just to be send.

These are the steps needed to transmit a multicast packet:
  1. In the first step an InetAddress for the specified Multicast Group IP Address is created.
  2. The next step is to construct a multicast socket and a dategram packet.
  3. 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();

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;
    }
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;
    }
}

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
The sender determines the size of each image slize, after that it sends the slices one by one over the network together with the above described additional header information.

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.

Sender and Reveiver
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;
            }

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();
                }
            }
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:

Running the Application

Download
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  Receiver
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.

Eeepc screeen on Ubuntu Transferring an image Ubuntu on eeePC  Nokia N800 running Jalimo

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

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


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! Face smile
(joschi)

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

 
Valid HTML 4.01 Transitional  Creative Commons License
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License.