/*
* SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package net.java.sip.communicator.impl.protocol.jabber;
import java.beans.*;
import java.io.*;
import java.net.*;
import java.util.*;
import org.ice4j.*;
import org.ice4j.ice.*;
import org.ice4j.ice.harvest.*;
import org.ice4j.security.*;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.filter.*;
import org.jivesoftware.smack.packet.*;
import org.jivesoftware.smack.packet.IQ.*;
import org.jivesoftware.smack.util.StringUtils;
import net.java.sip.communicator.impl.protocol.jabber.extensions.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.gtalk.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.jingleinfo.*;
import net.java.sip.communicator.service.httputil.*;
import net.java.sip.communicator.service.httputil.HttpUtils.HTTPResponseResult;
import net.java.sip.communicator.service.neomedia.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.media.*;
import net.java.sip.communicator.util.*;
/**
* <tt>TransportManager</tt>s gather local candidates for incoming and outgoing
* calls. Their work starts by calling a start method which, using the remote
* peer's session description, would start the harvest. Calling a second wrapup
* method would deliver the candidate harvest, possibly after blocking if it has
* not yet completed.
*
* @author Emil Ivov
* @author Lyubomir Marinov
* @author Sebastien Vincent
*/
public class TransportManagerGTalkImpl
extends TransportManager<CallPeerGTalkImpl>
{
/**
* The <tt>Logger</tt> used by the <tt>IceUdpTransportManager</tt>
* class and its instances for logging output.
*/
private static final Logger logger
= Logger.getLogger(TransportManagerGTalkImpl.class);
/**
* Default STUN server address.
*/
private static final String DEFAULT_STUN_SERVER_ADDRESS = "stun.jitsi.net";
/**
* Default STUN server port.
*/
private static final int DEFAULT_STUN_SERVER_PORT = 3478;
/**
* The generation of the candidates we are currently generating
*/
private int currentGeneration = 0;
/**
* The ID that we will be assigning to our next candidate. We use
* <tt>int</tt>s for interoperability reasons (Emil: I believe that GTalk
* uses <tt>int</tt>s. If that turns out not to be the case we can stop
* using <tt>int</tt>s here if that's an issue).
*/
private static int nextID = 1;
/**
* The ICE <tt>Component</tt> IDs in their common order used, for example,
* by <tt>DefaultStreamConnector</tt>, <tt>MediaStreamTarget</tt>.
*/
private static final int[] COMPONENT_IDS
= new int[] { Component.RTP, Component.RTCP };
/**
* The ICE agent that this transport manager would be using for ICE
* negotiation.
*/
private final Agent iceAgent;
/**
* Synchronization object.
*/
private final Object wrapupSyncRoot = new Object();
/**
* Creates a new instance of this transport manager, binding it to the
* specified peer.
*
* @param callPeer the {@link CallPeer} whose traffic we will be taking
* care of.
*/
public TransportManagerGTalkImpl(CallPeerGTalkImpl callPeer)
{
super(callPeer);
iceAgent = createIceAgent();
}
/**
* Returns the ID that we will be assigning to the next candidate we create.
*
* @return the next ID to use with a candidate.
*/
protected String getNextID()
{
return Integer.toString(nextID++);
}
/**
* Returns the generation that our current candidates belong to.
*
* @return the generation that we should assign to candidates that we are
* currently advertising.
*/
protected int getCurrentGeneration()
{
return currentGeneration;
}
/**
* Increments the generation that we are assigning candidates.
*/
protected void incrementGeneration()
{
currentGeneration++;
}
/**
* Returns the <tt>InetAddress</tt> that is most likely to be to be used
* as a next hop when contacting the specified <tt>destination</tt>. This is
* an utility method that is used whenever we have to choose one of our
* local addresses to put in the Via, Contact or (in the case of no
* registrar accounts) From headers.
*
* @param peer the CallPeer that we would contact.
*
* @return the <tt>InetAddress</tt> that is most likely to be to be used
* as a next hop when contacting the specified <tt>destination</tt>.
*
* @throws IllegalArgumentException if <tt>destination</tt> is not a valid
* host/IP/FQDN
*/
@Override
protected InetAddress getIntendedDestination(CallPeerGTalkImpl peer)
{
return peer.getProtocolProvider().getNextHop();
}
/**
* Request Google's Jingle info.
*
* @return list of servers
*/
public List<StunServerDescriptor> requestJingleInfo()
{
JingleInfoQueryIQ iq = new JingleInfoQueryIQ();
ProtocolProviderServiceJabberImpl provider =
getCallPeer().getProtocolProvider();
String accountIDService = provider.getAccountID().getService();
boolean jingleInfoIsSupported
= provider.isFeatureSupported(accountIDService,
JingleInfoQueryIQ.NAMESPACE);
final List<StunServerDescriptor> servers =
new ArrayList<StunServerDescriptor>();
final Object syncRoot = new Object();
// check for google:jingleinfo support
if(!jingleInfoIsSupported)
{
return servers;
}
if(logger.isDebugEnabled())
logger.debug("google:jingleinfo supported for " +
provider.getOurJID());
iq.setFrom(provider.getOurJID());
iq.setTo(StringUtils.parseBareAddress(
provider.getOurJID()));
iq.setType(Type.GET);
XMPPConnection connection = provider.getConnection();
PacketListener pl = new PacketListener()
{
public void processPacket(Packet p)
{
JingleInfoQueryIQ iq = (JingleInfoQueryIQ)p;
Iterator<PacketExtension> it = iq.getExtensions().iterator();
while(it.hasNext())
{
AbstractPacketExtension ext =
(AbstractPacketExtension)it.next();
if(ext.getElementName().equals(
StunPacketExtension.ELEMENT_NAME))
{
for(ServerPacketExtension e :
ext.getChildExtensionsOfType(
ServerPacketExtension.class))
{
StunServerDescriptor dsc =
new StunServerDescriptor(e.getHost(),
e.getUdp(), false, null, null);
servers.add(dsc);
}
}
else if(ext.getElementName().equals(
RelayPacketExtension.ELEMENT_NAME))
{
String token = ((RelayPacketExtension)ext).getToken();
for(ServerPacketExtension e :
ext.getChildExtensionsOfType(
ServerPacketExtension.class))
{
String headerNames[] = new String[2];
String headerValues[] = new String[2];
String addr = "http://" + e.getHost() +
"/create_session";
headerNames[0] = "X-Talk-Google-Relay-Auth";
headerNames[1] = "X-Google-Relay-Auth";
headerValues[0] = token;
headerValues[1] = token;
HTTPResponseResult res =
HttpUtils.openURLConnection(addr,
headerNames, headerValues);
Hashtable<String, String> relayData = null;
try
{
relayData =
parseGoogleRelay(res.getContentString());
}
catch (IOException excpt)
{
logger.info("HTTP query to " + e.getHost() +
"failed", excpt);
break;
}
String user = relayData.get("username");
String password = relayData.get("passsword");
StunServerDescriptor dsc =
new StunServerDescriptor(
relayData.get("relay"),
Integer.parseInt(
relayData.get("udpport")),
true,
user, password);
// not the RFC5766 TURN support
dsc.setOldTurn(true);
servers.add(dsc);
dsc = new StunServerDescriptor(
relayData.get("relay"),
Integer.parseInt(relayData.get("tcpport")),
true,
user,
password);
dsc.setOldTurn(true);
dsc.setProtocol("tcp");
servers.add(dsc);
dsc = new StunServerDescriptor(
relayData.get("relay"),
Integer.parseInt(
relayData.get("ssltcpport")),
true,
user,
password);
dsc.setOldTurn(true);
dsc.setProtocol("ssltcp");
servers.add(dsc);
}
}
}
synchronized(syncRoot)
{
syncRoot.notify();
}
}
};
connection.addPacketListener(pl,
new PacketFilter()
{
public boolean accept(Packet p)
{
if(p instanceof JingleInfoQueryIQ)
return true;
return false;
}
});
provider.getConnection().sendPacket(iq);
synchronized(syncRoot)
{
try
{
syncRoot.wait(2000);
}
catch(InterruptedException e)
{
}
}
connection.removePacketListener(pl);
return servers;
}
/**
* Parse HTTP response from Google relay.
*
* @param res content string
* @return String
*/
public Hashtable<String, String> parseGoogleRelay(String res)
{
// Separate each line
StringTokenizer tokenizer = new StringTokenizer(res, "\n");
Hashtable<String, String> ret = new Hashtable<String, String>();
while(tokenizer.hasMoreTokens())
{
String token = tokenizer.nextToken();
if(token.startsWith("relay.ip="))
{
ret.put("relay", token.substring(token.indexOf("=") + 1));
}
else if(token.startsWith("relay.udp_port="))
{
ret.put("udpport", token.substring(token.indexOf("=") + 1));
}
else if(token.startsWith("relay.tcp_port="))
{
ret.put("tcpport", token.substring(token.indexOf("=") + 1));
}
else if(token.startsWith("relay.ssltcp_port="))
{
ret.put("ssltcpport", token.substring(token.indexOf("=") + 1));
}
else if(token.startsWith("username="))
{
ret.put("username", token.substring(token.indexOf("=") + 1));
}
else if(token.startsWith("password="))
{
ret.put("password", token.substring(token.indexOf("=") + 1));
}
}
return ret;
}
/**
* Creates the ICE agent that we would be using in this transport manager
* for all negotiation.
*
* @return the ICE agent to use for all the ICE negotiation that this
* transport manager would be going through
*/
private Agent createIceAgent()
{
CallPeerGTalkImpl peer = getCallPeer();
Agent agent = new Agent(CompatibilityMode.GTALK);
List<StunServerDescriptor> servers = null;
boolean atLeastOneStunServer = false;
ProtocolProviderServiceJabberImpl provider = peer.getProtocolProvider();
JabberAccountID accID = (JabberAccountID)provider.getAccountID();
agent.setControlling(!peer.isInitiator());
servers = requestJingleInfo();
for(StunServerDescriptor desc : servers)
{
Transport transport;
/* Google ssltcp mode is in fact pseudo-SSL (just the client/server
* hello)
*/
if(desc.getProtocol().equals("ssltcp"))
{
transport = Transport.TCP;
}
else
{
transport = Transport.parse(desc.getProtocol());
}
TransportAddress addr = new TransportAddress(
desc.getAddress(),
desc.getPort(),
transport);
StunCandidateHarvester harvester = null;
if(desc.isTurnSupported())
{
logger.info("Google TURN descriptor");
/* Google relay server used a special way to allocate
* address (token + HTTP request, ...) and they don't support
* long-term authentication
*/
if(desc.isOldTurn())
{
logger.info("new Google TURN harvester");
if(desc.getProtocol().equals("ssltcp"))
{
harvester = new GoogleTurnSSLCandidateHarvester(
addr, new String(desc.getUsername()));
}
else
harvester = new GoogleTurnCandidateHarvester(
addr, new String(desc.getUsername()));
}
else
{
harvester
= new TurnCandidateHarvester(
addr,
new LongTermCredential(
desc.getUsername(),
desc.getPassword()));
atLeastOneStunServer = true;
}
}
else
{
// take only the first STUN server for now
if(atLeastOneStunServer)
continue;
//this is a STUN only server
harvester = new StunCandidateHarvester(addr);
atLeastOneStunServer = true;
logger.info("Found Google STUN server " + harvester);
}
if(harvester != null)
{
agent.addCandidateHarvester(harvester);
}
}
if(!atLeastOneStunServer)
{
/* we have no configured or discovered STUN server so takes the
* default provided by us if user allows it
*/
if(accID.isUseDefaultStunServer())
{
TransportAddress addr = new TransportAddress(
DEFAULT_STUN_SERVER_ADDRESS,
DEFAULT_STUN_SERVER_PORT,
Transport.UDP);
StunCandidateHarvester harvester =
new StunCandidateHarvester(addr);
if(harvester != null)
{
agent.addCandidateHarvester(harvester);
}
}
}
if(accID.isUPNPEnabled())
{
UPNPHarvester harvester = new UPNPHarvester();
if(harvester != null)
{
agent.addCandidateHarvester(harvester);
}
}
return agent;
}
/**
* Initializes a new <tt>StreamConnector</tt> to be used as the
* <tt>connector</tt> of the <tt>MediaStream</tt> with a specific
* <tt>MediaType</tt>.
*
* @param mediaType the <tt>MediaType</tt> of the <tt>MediaStream</tt> which
* is to have its <tt>connector</tt> set to the returned
* <tt>StreamConnector</tt>
* @return a new <tt>StreamConnector</tt> to be used as the
* <tt>connector</tt> of the <tt>MediaStream</tt> with the specified
* <tt>MediaType</tt>
* @throws OperationFailedException if anything goes wrong while
* initializing the new <tt>StreamConnector</tt>
*/
@Override
protected StreamConnector createStreamConnector(MediaType mediaType)
throws OperationFailedException
{
DatagramSocket[] streamConnectorSockets
= getStreamConnectorSockets(mediaType);
Socket[] tcpStreamConnectorSockets
= getStreamConnectorTCPSockets(mediaType);
/*
* XXX If the iceAgent has not completed (yet), go with a default
* StreamConnector (until it completes).
*/
if(tcpStreamConnectorSockets == null)
{
return
(streamConnectorSockets == null)
? super.createStreamConnector(mediaType)
: new DefaultStreamConnector(
streamConnectorSockets[0 /* RTP */],
streamConnectorSockets[1 /* RTCP */]);
}
else
{
return
(tcpStreamConnectorSockets == null)
? super.createStreamConnector(mediaType)
: new DefaultTCPStreamConnector(
tcpStreamConnectorSockets[0 /* RTP */],
tcpStreamConnectorSockets[1 /* RTCP */]);
}
}
/**
* Gets the <tt>StreamConnector</tt> to be used as the <tt>connector</tt> of
* the <tt>MediaStream</tt> with a specific <tt>MediaType</tt>.
*
* @param mediaType the <tt>MediaType</tt> of the <tt>MediaStream</tt> which
* is to have its <tt>connector</tt> set to the returned
* <tt>StreamConnector</tt>
* @return the <tt>StreamConnector</tt> to be used as the <tt>connector</tt>
* of the <tt>MediaStream</tt> with the specified <tt>MediaType</tt>
* @throws OperationFailedException if anything goes wrong while
* initializing the requested <tt>StreamConnector</tt>
* @see net.java.sip.communicator.service.protocol.media.TransportManager#
* getStreamConnector(MediaType)
*/
@Override
public StreamConnector getStreamConnector(MediaType mediaType)
throws OperationFailedException
{
StreamConnector streamConnector = super.getStreamConnector(mediaType);
/*
* Since the super caches the StreamConnectors, make sure that the
* returned one is up-to-date with the iceAgent.
*/
if (streamConnector != null && streamConnector.getProtocol() ==
StreamConnector.Protocol.UDP)
{
DatagramSocket[] streamConnectorSockets
= getStreamConnectorSockets(mediaType);
/*
* XXX If the iceAgent has not completed (yet), go with the default
* StreamConnector (until it completes).
*/
if ((streamConnectorSockets != null)
&& ((streamConnector.getDataSocket()
!= streamConnectorSockets[0 /* RTP */])
|| (streamConnector.getControlSocket()
!= streamConnectorSockets[1 /* RTCP */])))
{
// Recreate the StreamConnector for the specified mediaType.
closeStreamConnector(mediaType);
streamConnector = super.getStreamConnector(mediaType);
}
}
else if (streamConnector != null && streamConnector.getProtocol() ==
StreamConnector.Protocol.TCP)
{
Socket[] streamConnectorSockets
= getStreamConnectorTCPSockets(mediaType);
/*
* XXX If the iceAgent has not completed (yet), go with the default
* StreamConnector (until it completes).
*/
if ((streamConnectorSockets != null)
&& ((streamConnector.getDataTCPSocket()
!= streamConnectorSockets[0 /* RTP */])
|| (streamConnector.getControlTCPSocket()
!= streamConnectorSockets[1 /* RTCP */])))
{
// Recreate the StreamConnector for the specified mediaType.
closeStreamConnector(mediaType);
streamConnector = super.getStreamConnector(mediaType);
}
}
return streamConnector;
}
/**
* Gets an array of <tt>Socket</tt>s which represents the sockets to
* be used by the <tt>StreamConnector</tt> with the specified
* <tt>MediaType</tt> in the order of {@link #COMPONENT_IDS} if
* {@link #iceAgent} has completed.
*
* @param mediaType the <tt>MediaType</tt> of the <tt>StreamConnector</tt>
* for which the <tt>Socket</tt>s are to be returned
* @return an array of <tt>Socket</tt>s which represents the sockets
* to be used by the <tt>StreamConnector</tt> which the specified
* <tt>MediaType</tt> in the order of {@link #COMPONENT_IDS} if
* {@link #iceAgent} has completed; otherwise, <tt>null</tt>
*/
private Socket[] getStreamConnectorTCPSockets(MediaType mediaType)
{
String mediaName = null;
if(mediaType == MediaType.AUDIO)
{
mediaName = "rtp";
}
else if(mediaType == MediaType.VIDEO)
{
mediaName = "video_rtp";
}
else
{
logger.error("Not an audio/rtp mediatype");
return null;
}
IceMediaStream stream = iceAgent.getStream(mediaName);
if (stream != null)
{
Socket[] streamConnectorSockets = new Socket[COMPONENT_IDS.length];
int streamConnectorSocketCount = 0;
for (int i = 0; i < COMPONENT_IDS.length; i++)
{
Component component = stream.getComponent(COMPONENT_IDS[i]);
if (component != null)
{
CandidatePair selectedPair = component.getSelectedPair();
if (selectedPair != null)
{
Socket streamConnectorSocket
= selectedPair.getLocalCandidate().
getSocket();
if (streamConnectorSocket != null)
{
streamConnectorSockets[i] = streamConnectorSocket;
streamConnectorSocketCount++;
}
}
}
}
if (streamConnectorSocketCount > 0)
{
return streamConnectorSockets;
}
}
return null;
}
/**
* Gets an array of <tt>DatagramSocket</tt>s which represents the sockets to
* be used by the <tt>StreamConnector</tt> with the specified
* <tt>MediaType</tt> in the order of {@link #COMPONENT_IDS} if
* {@link #iceAgent} has completed.
*
* @param mediaType the <tt>MediaType</tt> of the <tt>StreamConnector</tt>
* for which the <tt>DatagramSocket</tt>s are to be returned
* @return an array of <tt>DatagramSocket</tt>s which represents the sockets
* to be used by the <tt>StreamConnector</tt> which the specified
* <tt>MediaType</tt> in the order of {@link #COMPONENT_IDS} if
* {@link #iceAgent} has completed; otherwise, <tt>null</tt>
*/
private DatagramSocket[] getStreamConnectorSockets(MediaType mediaType)
{
String mediaName = null;
if(mediaType == MediaType.AUDIO)
{
mediaName = "rtp";
}
else if(mediaType == MediaType.VIDEO)
{
mediaName = "video_rtp";
}
else
{
logger.error("Not an audio/rtp mediatype");
return null;
}
IceMediaStream stream = iceAgent.getStream(mediaName);
if (stream != null)
{
DatagramSocket[] streamConnectorSockets
= new DatagramSocket[COMPONENT_IDS.length];
int streamConnectorSocketCount = 0;
for (int i = 0; i < COMPONENT_IDS.length; i++)
{
Component component = stream.getComponent(COMPONENT_IDS[i]);
if (component != null)
{
CandidatePair selectedPair = component.getSelectedPair();
if (selectedPair != null)
{
DatagramSocket streamConnectorSocket
= selectedPair.getLocalCandidate().
getDatagramSocket();
if (streamConnectorSocket != null)
{
streamConnectorSockets[i] = streamConnectorSocket;
streamConnectorSocketCount++;
}
}
}
}
if (streamConnectorSocketCount > 0)
{
return streamConnectorSockets;
}
}
return null;
}
/**
* Implements {@link TransportManagerJabberImpl#getStreamTarget(MediaType)}.
* Gets the <tt>MediaStreamTarget</tt> to be used as the <tt>target</tt> of
* the <tt>MediaStream</tt> with a specific <tt>MediaType</tt>.
*
* @param mediaType the <tt>MediaType</tt> of the <tt>MediaStream</tt> which
* is to have its <tt>target</tt> set to the returned
* <tt>MediaStreamTarget</tt>
* @return the <tt>MediaStreamTarget</tt> to be used as the <tt>target</tt>
* of the <tt>MediaStream</tt> with the specified <tt>MediaType</tt>
* @see TransportManagerJabberImpl#getStreamTarget(MediaType)
*/
public MediaStreamTarget getStreamTarget(MediaType mediaType)
{
IceMediaStream stream = null;
MediaStreamTarget streamTarget = null;
String mediaName = null;
if(mediaType == MediaType.AUDIO)
{
mediaName = "rtp";
}
else if(mediaType == MediaType.VIDEO)
{
mediaName = "video_rtp";
}
else
{
logger.error("Not an audio/rtp mediatype");
return null;
}
stream = iceAgent.getStream(mediaName);
if (stream != null)
{
InetSocketAddress[] streamTargetAddresses
= new InetSocketAddress[COMPONENT_IDS.length];
int streamTargetAddressCount = 0;
for (int i = 0; i < COMPONENT_IDS.length; i++)
{
Component component = stream.getComponent(COMPONENT_IDS[i]);
if (component != null)
{
CandidatePair selectedPair = component.getSelectedPair();
if (selectedPair != null)
{
InetSocketAddress streamTargetAddress
= selectedPair
.getRemoteCandidate()
.getTransportAddress();
if (streamTargetAddress != null)
{
streamTargetAddresses[i] = streamTargetAddress;
streamTargetAddressCount++;
}
}
}
}
if (streamTargetAddressCount > 0)
{
streamTarget
= new MediaStreamTarget(
streamTargetAddresses[0 /* RTP */],
streamTargetAddresses[1 /* RTCP */]);
}
}
return streamTarget;
}
/**
* Creates an {@link IceMediaStream} with the specified <tt>media</tt>
* name.
*
* @param media the name of the stream we'd like to create.
*
* @param rtcp if true allocate an RTCP port
*
* @return the newly created {@link IceMediaStream}
*
* @throws OperationFailedException if binding on the specified media stream
* fails for some reason.
*/
private IceMediaStream createIceStream(String media, boolean rtcp)
throws OperationFailedException
{
IceMediaStream stream;
try
{
//the following call involves STUN processing so it may take a while
stream = iceAgent.createMediaStream(media);
int rtpPort = nextMediaPortToTry;
//rtp
iceAgent.createComponent(stream, Transport.UDP, rtpPort, rtpPort,
rtpPort + 100);
if(rtcp)
iceAgent.createComponent(stream, Transport.UDP,
rtpPort + 1, rtpPort + 1, rtpPort + 101);
}
catch (Exception ex)
{
throw new OperationFailedException(
"Failed to initialize stream " + media,
OperationFailedException.INTERNAL_ERROR,
ex);
}
//let's now update the next port var as best we can: we would assume
//that all local candidates are bound on the same port and set it
//to the one just above. if the assumption is wrong the next bind
//would simply include one more bind retry.
try
{
nextMediaPortToTry = stream.getComponent(rtcp ? Component.RTCP :
Component.RTP)
.getLocalCandidates().get(0)
.getTransportAddress().getPort() + 1;
}
catch(Throwable t)
{
//hey, we were just trying to be nice. if that didn't work for
//some reason we really can't be held responsible!
logger.debug("Determining next port didn't work: ", t);
}
return stream;
}
/**
* Starts transport candidate harvest. This method should complete rapidly
* and, in case of lengthy procedures like STUN/TURN/UPnP candidate harvests
* are necessary, they should be executed in a separate thread.
*
* @param ourAnswer the content descriptions that we should be adding our
* transport lists to (although not necessarily in this very instance).
* @param candidatesSender the <tt>CandidatesSender</tt> to be used by
* this <tt>TransportManagerGTalkImpl</tt> to send <tt>candidates</tt>
* <tt>SessionIQ</tt>s from the local peer to the remote peer if this
* <tt>TransportManagerGTalkImpl</tt> wishes to utilize
* <tt>candidates</tt>.
*
* @throws OperationFailedException if we fail to allocate a port number.
*/
public void startCandidateHarvest(
List<PayloadTypePacketExtension> ourAnswer,
CandidatesSender candidatesSender)
throws OperationFailedException
{
boolean audio = false;
boolean video = false;
List<GTalkCandidatePacketExtension> candidates
= new LinkedList<GTalkCandidatePacketExtension>();
synchronized(wrapupSyncRoot)
{
for(PayloadTypePacketExtension ext : ourAnswer)
{
if(ext.getNamespace().equals(
SessionIQProvider.GTALK_AUDIO_NAMESPACE))
{
audio = true;
}
else if(ext.getNamespace().equals(
SessionIQProvider.GTALK_VIDEO_NAMESPACE))
{
video = true;
}
}
if(audio)
{
IceMediaStream stream = createIceStream("rtp", video);
candidates.addAll(GTalkPacketFactory.createCandidates("rtp",
stream));
}
if(video)
{
IceMediaStream stream = createIceStream("video_rtp", true);
candidates.addAll(
GTalkPacketFactory.createCandidates("video_rtp", stream));
}
/* send candidates */
candidatesSender.sendCandidates(candidates);
}
}
/**
* Notifies the transport manager that it should conclude candidate
* harvesting as soon as possible.
*/
public void wrapupCandidateHarvest()
{
}
/**
* Starts the connectivity establishment of this
* <tt>TransportManagerGTalkImpl</tt> i.e. checks the connectivity between
* the local and the remote peers given the remote counterpart of the
* negotiation between them.
*
* @param remote the collection of <tt>CandidatePacketExtension</tt>s which
* represents the remote counterpart of the negotiation between the local
* and the remote peer
* @return <tt>true</tt> if connectivity establishment has been started in
* response to the call; otherwise, <tt>false</tt>.
*/
public boolean startConnectivityEstablishment(
Iterable<GTalkCandidatePacketExtension> remote)
{
if (IceProcessingState.COMPLETED.equals(iceAgent.getState())/* ||
IceProcessingState.FAILED.equals(iceAgent.getState())*/)
{
return true;
}
/* If ICE is already running, we try to update the checklists with
* the candidates. Note that this is a best effort.
*/
if (IceProcessingState.RUNNING.equals(iceAgent.getState()))
{
if(logger.isInfoEnabled())
{
logger.info("Update Google ICE remote candidates");
}
if(remote == null)
{
return false;
}
for(GTalkCandidatePacketExtension candidate : remote)
{
String name = candidate.getName();
int numComponent = 0;
// change name to retrieve properly the ICE media stream
if(name.equals("rtp"))
{
numComponent = Component.RTP;
}
else if(name.equals("rtcp"))
{
name = "rtp";
numComponent = Component.RTCP;
}
else if(name.equals("video_rtp"))
{
numComponent = Component.RTP;
}
else if(name.equals("video_rtcp"))
{
name = "video_rtp";
numComponent = Component.RTCP;
}
IceMediaStream stream = iceAgent.getStream(name);
if(stream == null)
{
continue;
}
/* Different candidates may have different ufrag/password */
String ufrag = candidate.getUsername();
//String password = candidate.getPassword();
/*
* Is the remote candidate from the current generation of
* the iceAgent?
*/
if (candidate.getGeneration() != iceAgent.getGeneration())
continue;
// XXX UDP only for the moment as ice4j.org does not support
// TCP yet
if(candidate.getProtocol().equalsIgnoreCase(
"ssltcp"))
continue;
Component component
= stream.getComponent(numComponent);
RemoteCandidate remoteCandidate = new RemoteCandidate(
new TransportAddress(
candidate.getAddress(),
candidate.getPort(),
Transport.parse(
candidate.getProtocol())),
component,
org.ice4j.ice.CandidateType.parse(
candidate.getType().toString()),
"0",
(long)(candidate.getPreference() * 1000),
ufrag);
component.addUpdateRemoteCandidate(remoteCandidate);
}
/* update all components of all streams */
for(IceMediaStream stream : iceAgent.getStreams())
{
for(Component component : stream.getComponents())
{
component.updateRemoteCandidate();
}
}
return false;
}
int generation = iceAgent.getGeneration();
boolean startConnectivityEstablishment = false;
if(remote == null)
{
return false;
}
for(GTalkCandidatePacketExtension candidate : remote)
{
String name = candidate.getName();
int numComponent = 0;
// change name to retrieve properly the ICE media stream
if(name.equals("rtp"))
{
numComponent = Component.RTP;
}
else if(name.equals("rtcp"))
{
name = "rtp";
numComponent = Component.RTCP;
}
else if(name.equals("video_rtp"))
{
numComponent = Component.RTP;
}
else if(name.equals("video_rtcp"))
{
name = "video_rtp";
numComponent = Component.RTCP;
}
IceMediaStream stream = null;
synchronized(wrapupSyncRoot)
{
stream = iceAgent.getStream(name);
}
if(stream == null)
{
continue;
}
/* Different candidates may have different ufrag/password */
String ufrag = candidate.getUsername();
//String password = candidate.getPassword();
/*
* Is the remote candidate from the current generation of
* the iceAgent?
*/
if (candidate.getGeneration() != generation)
continue;
if(candidate.getProtocol().equalsIgnoreCase("ssltcp"))
continue;
Component component
= stream.getComponent(numComponent);
RemoteCandidate remoteCandidate = new RemoteCandidate(
new TransportAddress(
candidate.getAddress(),
candidate.getPort(),
Transport.parse(
candidate.getProtocol())),
component,
org.ice4j.ice.CandidateType.parse(
candidate.getType().toString()),
"0",
(long)(candidate.getPreference() * 1000),
ufrag);
component.addRemoteCandidate(remoteCandidate);
startConnectivityEstablishment = true;
}
if (startConnectivityEstablishment)
{
/*
* Once again because the ICE Agent does not support adding
* candidates after the connectivity establishment has been started
* and because multiple transport-info JingleIQs may be used to send
* the whole set of transport candidates from the remote peer to the
* local peer, do not really start the connectivity establishment
* until we have at least two remote candidates (i.e. local and
* stun) per ICE Component.
*/
for (IceMediaStream stream : iceAgent.getStreams())
{
for (Component component : stream.getComponents())
{
if (component.getRemoteCandidateCount() < 1)
{
startConnectivityEstablishment = false;
break;
}
}
if (!startConnectivityEstablishment)
break;
}
if (startConnectivityEstablishment)
{
iceAgent.startConnectivityEstablishment();
return true;
}
}
return false;
}
/**
* Waits for the associated ICE <tt>Agent</tt> to finish any started
* connectivity checks.
*
* @throws OperationFailedException if ICE processing has failed
*/
public void wrapupConnectivityEstablishment()
throws OperationFailedException
{
final Object iceProcessingStateSyncRoot = new Object();
PropertyChangeListener stateChangeListener
= new PropertyChangeListener()
{
public void propertyChange(PropertyChangeEvent evt)
{
Object newValue = evt.getNewValue();
if (IceProcessingState.COMPLETED.equals(newValue)
|| IceProcessingState.FAILED.equals(newValue)
|| IceProcessingState.TERMINATED.equals(newValue))
{
if (logger.isTraceEnabled())
logger.trace("ICE " + newValue);
Agent iceAgent = (Agent) evt.getSource();
iceAgent.removeStateChangeListener(this);
if (iceAgent == TransportManagerGTalkImpl.this.iceAgent)
{
synchronized (iceProcessingStateSyncRoot)
{
iceProcessingStateSyncRoot.notify();
}
}
}
}
};
iceAgent.addStateChangeListener(stateChangeListener);
// Wait for the connectivity checks to finish if they have been started.
boolean interrupted = false;
synchronized (iceProcessingStateSyncRoot)
{
while (IceProcessingState.RUNNING.equals(iceAgent.getState()))
{
try
{
iceProcessingStateSyncRoot.wait();
}
catch (InterruptedException ie)
{
interrupted = true;
}
}
}
if (interrupted)
Thread.currentThread().interrupt();
/*
* Make sure stateChangeListener is removed from iceAgent in case its
* #propertyChange(PropertyChangeEvent) has never been executed.
*/
iceAgent.removeStateChangeListener(stateChangeListener);
/* check the state of ICE processing and throw exception if failed */
if(iceAgent.getState().equals(IceProcessingState.FAILED))
{
throw new OperationFailedException(
"Could not establish connection (ICE failed)",
OperationFailedException.GENERAL_ERROR);
}
}
/**
* Close this transport manager and release resources.
*/
public void close()
{
if(iceAgent != null)
{
logger.info("Close transport manager agent");
iceAgent.free();
}
}
}