Please 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:
/*** 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.
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:
// 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:
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:
// 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:
packet.setAddress(hostname);
packet.setPort(port);
wrapper.send(packet);
Receiving information is easier, no address or port information is needed:
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.