17 April, 2015

ICE4J Networking Tutorial Part 1
By: Matthew Jackson

Jitsi LogoPlease read a simple understanding of STUN, TURN, & ICE if you are new to the concepts

Get the Ice4J Library: If you don't have the ice4j jar or source, you can get it from [the official repository here on Google] here on GitHub! I also have a github repository with a few changes I made, but I don't keep it up to date with the official repository yet, so use the official repository for now, I noticed that the original library does limit receivable chunks of data to 1500 bytes, which for most people isn't an issue, mine allows much more.

The best way to understand ICE4J is to look at some code:

Agent agent = new Agent(); // A simple ICE Agent

/*** Setup the STUN servers: ***/
String[] hostnames = new String[] {"jitsi.org","numb.viagenie.ca","stun.ekiga.net"};
// Look online for actively working public STUN Servers. You can find free servers.
// Now add these URLS as Stun Servers with standard 3478 port for STUN servrs.
for(String hostname: hostnames){
   try {
      // InetAddress qualifies a url to an IP Address, if you have an error here, make sure the url is reachable and correct
      TransportAddress ta = new TransportAddress(InetAddress.getByName(hostname), 3478, Transport.UDP);
      // Currently Ice4J only supports UDP and will throw an Error otherwise
      agent.addCandidateHarvester(new StunCandidateHarvester(ta));
   } catch (Exception e) { e.printStackTrace();}
}

Now you have your Agent setup. The agent will now be able to know its IP Address and Port once you attempt to connect. You do need to setup Streams on the Agent to open a flow of information on a specific port.

IceMediaStream stream = agent.createMediaStream("audio");
int port = 5000; // Choose any port
agent.createComponent(stream, Transport.UDP, port, port, port+100);
// The three last arguments are: preferredPort, minPort, maxPort

Now we have our port and we have our stream to allow for information to flow. The issue is that once we have all the information we need each computer to get the remote computer's information. Of course how do you get that information if you can't connect? There might be a few ways, but the easiest with just ICE4J is to POST the information to your public sever and retrieve the information. I even use a simple PHP server I wrote to store and spit out information.

What information and how do you re-construct it?

Well we can borrow some code from test.SdpUtils:

String toSend = SdpUtils.getSDPDescription(agent); //Each computer sends this information
// This information describes all the possible IP addresses and ports

The String "toSend" should be sent to a server. You need to write a PHP, Java or any server. It should be able to have this String posted to a database. Each program checks to see if another program is requesting a call. If it is, they can both post this "toSend" information and then read eachother's "toSend" SDP string. After you get this information about the remote computer do the following for ice4j to build the connection:

String remoteReceived = ""; // This information was grabbed from the server, and shouldn't be empty.
SdpUtils.parseSDP(agent, remoteReceived); // This will add the remote information to the agent.

Hopefully now your Agent is totally setup. Now we need to start the connections:

agent.addAgentStateChangeListener(new StateListener()); // We will define this class soon
// You need to listen for state change so that once connected you can then use the socket.
agent.startConnectivityEstablishment(); // This will do all the work for you to connect

StateListener class to react to events about your connection:

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.net.InetAddress;

import org.ice4j.TransportAddress;
import org.ice4j.ice.Agent;
import org.ice4j.ice.CandidatePair;
import org.ice4j.ice.Component;
import org.ice4j.ice.IceMediaStream;
import org.ice4j.ice.IceProcessingState;
import org.ice4j.socket.IceSocketWrapper;

public class StateListener implements PropertyChangeListener {

   private InetAddress hostname;
   int port;
   @Override
   public void propertyChange(PropertyChangeEvent evt) {
      if(evt.getSource() instanceof Agent){
         Agent agent = (Agent) evt.getSource();
         if(agent.getState().equals(IceProcessingState.TERMINATED)) {
            // Your agent is connected. Terminated means ready to communicate
            for (IceMediaStream stream: agent.getStreams()) {
               if (stream.getName().contains("audio")) {
                  Component rtpComponent = stream.getComponent(org.ice4j.ice.Component.RTP);
                  CandidatePair rtpPair = rtpComponent.getSelectedPair();
                  // We use IceSocketWrapper, but you can just use the UDP socket
                  // The advantage is that you can change the protocol from UDP to TCP easily
                  // Currently only UDP exists so you might not need to use the wrapper.

                  IceSocketWrapper wrapper  = rtpPair.getIceSocketWrapper();
                  // Get information about remote address for packet settings
                  TransportAddress ta = rtpPair.getRemoteCandidate().getTransportAddress();
                  hostname = ta.getAddress();
                  port = ta.getPort();
               }
            }
         }
      }
   }
}

Once you have the IceSocketWrapper or UDP Socket you can just send and receive as usual. When you send you do need to setup the correct hostname and ports within the packet as follows:

DatagramPacket packet = new DatagramPacket(new byte[10000],10000);
packet.setAddress(hostname);
packet.setPort(port);
wrapper.send(packet);

Receiving information is easier, no address or port information is needed:

DatagramPacket packet = new DatagramPacket(new byte[10000],10000);
wrapper.receive(packet); // This will block until you receive data that you can use.

It is a good idea to always try to keep both sides having the same length of byte array otherwise you might lose some data. This is because the whole UDP packet isn't being stored because it will overflow your array.

You should know that in Java UDP packets are broken up and sent to the other computer where they are re-assembled. If one of the pieces are missing than the whole packet will be discarded.

 

Part 2 will discuss how to format the DatagramPacket to be able to properly pass through ice4j without it being affected.

Tags: ICE4J, Java, Networking