/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Copyright @ 2015 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.java.sip.communicator.impl.protocol.jabber;
import java.beans.*;
import java.net.*;
import java.util.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.CandidateType;
import net.java.sip.communicator.service.netaddr.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.media.*;
import net.java.sip.communicator.util.*;
import net.java.sip.communicator.util.Logger;
import org.ice4j.*;
import org.ice4j.ice.*;
import org.ice4j.ice.harvest.*;
import org.ice4j.security.*;
import org.jitsi.service.neomedia.*;
import org.jitsi.util.*;
import org.jivesoftware.smack.packet.*;
import org.xmpp.jnodes.smack.*;
/**
* A {@link TransportManagerJabberImpl} implementation that would use ICE for
* candidate management.
*
* @author Emil Ivov
* @author Lyubomir Marinov
* @author Sebastien Vincent
*/
public class IceUdpTransportManager
extends TransportManagerJabberImpl
implements PropertyChangeListener
{
/**
* The Logger used by the IceUdpTransportManager
* class and its instances for logging output.
*/
private static final Logger logger
= Logger.getLogger(IceUdpTransportManager.class);
/**
* The ICE Component IDs in their common order used, for example,
* by DefaultStreamConnector, MediaStreamTarget.
*/
private static final int[] COMPONENT_IDS
= new int[] { Component.RTP, Component.RTCP };
/**
* This is where we keep our answer between the time we get the offer and
* are ready with the answer.
*/
protected List cpeList;
/**
* The ICE agent that this transport manager would be using for ICE
* negotiation.
*/
protected final Agent iceAgent;
/**
* Default STUN server address.
*/
protected static final String DEFAULT_STUN_SERVER_ADDRESS = "stun.jitsi.net";
/**
* Default STUN server port.
*/
protected static final int DEFAULT_STUN_SERVER_PORT = 3478;
/**
* 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 IceUdpTransportManager(CallPeerJabberImpl callPeer)
{
super(callPeer);
iceAgent = createIceAgent();
iceAgent.addStateChangeListener(this);
}
/**
* 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
*/
protected Agent createIceAgent()
{
long startGatheringHarvesterTime = System.currentTimeMillis();
CallPeerJabberImpl peer = getCallPeer();
ProtocolProviderServiceJabberImpl provider = peer.getProtocolProvider();
NetworkAddressManagerService namSer = getNetAddrMgr();
boolean atLeastOneStunServer = false;
Agent agent = namSer.createIceAgent();
/*
* XEP-0176: the initiator MUST include the ICE-CONTROLLING attribute,
* the responder MUST include the ICE-CONTROLLED attribute.
*/
agent.setControlling(!peer.isInitiator());
//we will now create the harvesters
JabberAccountIDImpl accID
= (JabberAccountIDImpl) provider.getAccountID();
if (accID.isStunServerDiscoveryEnabled())
{
//the default server is supposed to use the same user name and
//password as the account itself.
String username
= org.jivesoftware.smack.util.StringUtils.parseName(
provider.getOurJID());
String password
= JabberActivator.getProtocolProviderFactory().loadPassword(
accID);
UserCredentials credentials = provider.getUserCredentials();
if(credentials != null)
password = credentials.getPasswordAsString();
// ask for password if not saved
if (password == null)
{
//create a default credentials object
credentials = new UserCredentials();
credentials.setUserName(accID.getUserID());
//request a password from the user
credentials
= provider.getAuthority().obtainCredentials(
accID.getDisplayName(),
credentials,
SecurityAuthority.AUTHENTICATION_REQUIRED);
// in case user has canceled the login window
if(credentials == null)
return null;
//extract the password the user passed us.
char[] pass = credentials.getPassword();
// the user didn't provide us a password (i.e. canceled the
// operation)
if(pass == null)
return null;
password = new String(pass);
if (credentials.isPasswordPersistent())
{
JabberActivator.getProtocolProviderFactory()
.storePassword(accID, password);
}
}
StunCandidateHarvester autoHarvester
= namSer.discoverStunServer(
accID.getService(),
StringUtils.getUTF8Bytes(username),
StringUtils.getUTF8Bytes(password));
if (logger.isInfoEnabled())
logger.info("Auto discovered harvester is " + autoHarvester);
if (autoHarvester != null)
{
atLeastOneStunServer = true;
agent.addCandidateHarvester(autoHarvester);
}
}
//now create stun server descriptors for whatever other STUN/TURN
//servers the user may have set.
for(StunServerDescriptor desc : accID.getStunServers())
{
TransportAddress addr
= new TransportAddress(
desc.getAddress(),
desc.getPort(),
Transport.UDP);
// if we get STUN server from automatic discovery, it may just
// be server name (i.e. stun.domain.org) and it may be possible that
// it cannot be resolved
if(addr.getAddress() == null)
{
logger.info("Unresolved address for " + addr);
continue;
}
StunCandidateHarvester harvester;
if(desc.isTurnSupported())
{
//Yay! a TURN server
harvester
= new TurnCandidateHarvester(
addr,
new LongTermCredential(
desc.getUsername(),
desc.getPassword()));
}
else
{
//this is a STUN only server
harvester = new StunCandidateHarvester(addr);
}
if (logger.isInfoEnabled())
logger.info("Adding pre-configured harvester " + harvester);
atLeastOneStunServer = true;
agent.addCandidateHarvester(harvester);
}
if(!atLeastOneStunServer && accID.isUseDefaultStunServer())
{
/* we have no configured or discovered STUN server so takes the
* default provided by us if user allows it
*/
TransportAddress addr
= new TransportAddress(
DEFAULT_STUN_SERVER_ADDRESS,
DEFAULT_STUN_SERVER_PORT,
Transport.UDP);
agent.addCandidateHarvester(new StunCandidateHarvester(addr));
}
/* Jingle nodes candidate */
if(accID.isJingleNodesRelayEnabled())
{
/* this method is blocking until Jingle Nodes auto-discovery (if
* enabled) finished
*/
SmackServiceNode serviceNode = provider.getJingleNodesServiceNode();
if(serviceNode != null)
{
agent.addCandidateHarvester(
new JingleNodesHarvester(serviceNode));
}
}
if(accID.isUPNPEnabled())
agent.addCandidateHarvester(new UPNPHarvester());
long stopGatheringHarvesterTime = System.currentTimeMillis();
if (logger.isInfoEnabled())
{
long gatheringHarvesterTime
= stopGatheringHarvesterTime - startGatheringHarvesterTime;
logger.info(
"End gathering harvester within " + gatheringHarvesterTime
+ " ms");
}
return agent;
}
/**
* {@inheritDoc}
*/
@Override
protected StreamConnector doCreateStreamConnector(MediaType mediaType)
throws OperationFailedException
{
/*
* If this instance is participating in a telephony conference utilizing
* the Jitsi Videobridge server-side technology that is organized by the
* local peer, then there is a single MediaStream (of the specified
* mediaType) shared among multiple TransportManagers and its
* StreamConnector may be determined only by the TransportManager which
* is establishing the connectivity with the Jitsi Videobridge server
* (as opposed to a CallPeer).
*/
TransportManagerJabberImpl delegate
= findTransportManagerEstablishingConnectivityWithJitsiVideobridge();
if ((delegate != null) && (delegate != this))
return delegate.doCreateStreamConnector(mediaType);
DatagramSocket[] streamConnectorSockets
= getStreamConnectorSockets(mediaType);
/*
* XXX If the iceAgent has not completed (yet), go with a default
* StreamConnector (until it completes).
*/
return
(streamConnectorSockets == null)
? super.doCreateStreamConnector(mediaType)
: new DefaultStreamConnector(
streamConnectorSockets[0 /* RTP */],
streamConnectorSockets[1 /* RTCP */]);
}
/**
* Gets the StreamConnector to be used as the connector of
* the MediaStream with a specific MediaType.
*
* @param mediaType the MediaType of the MediaStream which
* is to have its connector set to the returned
* StreamConnector
* @return the StreamConnector to be used as the connector
* of the MediaStream with the specified MediaType
* @throws OperationFailedException if anything goes wrong while
* initializing the requested StreamConnector
* @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)
{
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);
}
}
return streamConnector;
}
/**
* Gets an array of DatagramSockets which represents the sockets to
* be used by the StreamConnector with the specified
* MediaType in the order of {@link #COMPONENT_IDS} if
* {@link #iceAgent} has completed.
*
* @param mediaType the MediaType of the StreamConnector
* for which the DatagramSockets are to be returned
* @return an array of DatagramSockets which represents the sockets
* to be used by the StreamConnector which the specified
* MediaType in the order of {@link #COMPONENT_IDS} if
* {@link #iceAgent} has completed; otherwise, null
*/
private DatagramSocket[] getStreamConnectorSockets(MediaType mediaType)
{
IceMediaStream stream = iceAgent.getStream(mediaType.toString());
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 MediaStreamTarget to be used as the target of
* the MediaStream with a specific MediaType.
*
* @param mediaType the MediaType of the MediaStream which
* is to have its target set to the returned
* MediaStreamTarget
* @return the MediaStreamTarget to be used as the target
* of the MediaStream with the specified MediaType
* @see TransportManagerJabberImpl#getStreamTarget(MediaType)
*/
@Override
public MediaStreamTarget getStreamTarget(MediaType mediaType)
{
/*
* If this instance is participating in a telephony conference utilizing
* the Jitsi Videobridge server-side technology that is organized by the
* local peer, then there is a single MediaStream (of the specified
* mediaType) shared among multiple TransportManagers and its
* MediaStreamTarget may be determined only by the TransportManager
* which is establishing the connectivity with the Jitsi Videobridge
* server (as opposed to a CallPeer).
*/
TransportManagerJabberImpl delegate
= findTransportManagerEstablishingConnectivityWithJitsiVideobridge();
if ((delegate != null) && (delegate != this))
return delegate.getStreamTarget(mediaType);
IceMediaStream stream = iceAgent.getStream(mediaType.toString());
MediaStreamTarget streamTarget = null;
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;
}
/**
* Implements {@link TransportManagerJabberImpl#getXmlNamespace()}. Gets the
* XML namespace of the Jingle transport implemented by this
* TransportManagerJabberImpl.
*
* @return the XML namespace of the Jingle transport implemented by this
* TransportManagerJabberImpl
* @see TransportManagerJabberImpl#getXmlNamespace()
*/
@Override
public String getXmlNamespace()
{
return ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_ICE_UDP_1;
}
/**
* {@inheritDoc}
*/
protected PacketExtension createTransportPacketExtension()
{
return new IceUdpTransportPacketExtension();
}
/**
* {@inheritDoc}
*/
protected PacketExtension startCandidateHarvest(
ContentPacketExtension theirContent,
ContentPacketExtension ourContent,
TransportInfoSender transportInfoSender,
String media)
throws OperationFailedException
{
PacketExtension pe;
// Report the gathered candidate addresses.
if (transportInfoSender == null)
{
pe = createTransportForStartCandidateHarvest(media);
}
else
{
/*
* The candidates will be sent in transport-info so the transport of
* session-accept just has to be present, not populated with
* candidates.
*/
pe = createTransportPacketExtension();
/*
* Create the content to be sent in a transport-info. The transport
* is the only extension to be sent in transport-info so the content
* has the same attributes as in our answer and none of its
* non-transport extensions.
*/
ContentPacketExtension transportInfoContent
= new ContentPacketExtension();
for (String name : ourContent.getAttributeNames())
{
Object value = ourContent.getAttribute(name);
if (value != null)
transportInfoContent.setAttribute(name, value);
}
transportInfoContent.addChildExtension(
createTransportForStartCandidateHarvest(media));
/*
* We send each media content in separate transport-info. It is
* absolutely not mandatory (we can simply send all content in one
* transport-info) but the XMPP Jingle client Empathy (via
* telepathy-gabble), which is present on many Linux distributions
* and N900 mobile phone, has a bug when it receives more than one
* content in transport-info. The related bug has been fixed in
* mainstream but the Linux distributions have not updated their
* packages yet. That's why we made this modification to be fully
* interoperable with Empathy right now. In the future, we will get
* back to the original behavior: sending all content in one
* transport-info.
*/
Collection transportInfoContents
= new LinkedList();
transportInfoContents.add(transportInfoContent);
transportInfoSender.sendTransportInfo(transportInfoContents);
}
return pe;
}
/**
* 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. Candidate
* harvest would then need to be concluded in the
* {@link #wrapupCandidateHarvest()} method which would be called once we
* absolutely need the candidates.
*
* @param theirOffer a media description offer that we've received from the
* remote party and that we should use in case we need to know what
* transports our peer is using.
* @param ourAnswer the content descriptions that we should be adding our
* transport lists to (although not necessarily in this very instance).
* @param transportInfoSender the TransportInfoSender to be used by
* this TransportManagerJabberImpl to send transport-info
* JingleIQs from the local peer to the remote peer if this
* TransportManagerJabberImpl wishes to utilize
* transport-info. Local candidate addresses sent by this
* TransportManagerJabberImpl in transport-info are
* expected to not be included in the result of
* {@link #wrapupCandidateHarvest()}.
*
* @throws OperationFailedException if we fail to allocate a port number.
* @see TransportManagerJabberImpl#startCandidateHarvest(List, List,
* TransportInfoSender)
*/
@Override
public void startCandidateHarvest(
List theirOffer,
List ourAnswer,
TransportInfoSender transportInfoSender)
throws OperationFailedException
{
this.cpeList = ourAnswer;
super.startCandidateHarvest(theirOffer, ourAnswer, transportInfoSender);
}
/**
* Converts the ICE media stream and its local candidates into a
* {@link IceUdpTransportPacketExtension}.
*
* @param stream the {@link IceMediaStream} that we'd like to describe in
* XML.
*
* @return the {@link IceUdpTransportPacketExtension} that we
*/
protected PacketExtension createTransport(IceMediaStream stream)
{
IceUdpTransportPacketExtension transport
= new IceUdpTransportPacketExtension();
Agent iceAgent = stream.getParentAgent();
transport.setUfrag(iceAgent.getLocalUfrag());
transport.setPassword(iceAgent.getLocalPassword());
for(Component component : stream.getComponents())
{
for(Candidate> candidate : component.getLocalCandidates())
transport.addCandidate(createCandidate(candidate));
}
return transport;
}
/**
* {@inheritDoc}
*/
protected PacketExtension createTransport(String media)
throws OperationFailedException
{
IceMediaStream iceStream = iceAgent.getStream(media);
if (iceStream == null)
iceStream = createIceStream(media);
return createTransport(iceStream);
}
/**
* Creates a {@link CandidatePacketExtension} and initializes it so that it
* would describe the state of candidate
*
* @param candidate the ICE4J {@link Candidate} that we'd like to convert
* into an XMPP packet extension.
*
* @return a new {@link CandidatePacketExtension} corresponding to the state
* of the candidate candidate.
*/
private CandidatePacketExtension createCandidate(Candidate> candidate)
{
CandidatePacketExtension packet = new CandidatePacketExtension();
packet.setFoundation(candidate.getFoundation());
Component component = candidate.getParentComponent();
packet.setComponent(component.getComponentID());
packet.setProtocol(candidate.getTransport().toString());
packet.setPriority(candidate.getPriority());
packet.setGeneration(
component.getParentStream().getParentAgent().getGeneration());
TransportAddress transportAddress = candidate.getTransportAddress();
packet.setID(getNextID());
packet.setIP(transportAddress.getHostAddress());
packet.setPort(transportAddress.getPort());
packet.setType(CandidateType.valueOf(candidate.getType().toString()));
TransportAddress relAddr = candidate.getRelatedAddress();
if(relAddr != null)
{
packet.setRelAddr(relAddr.getHostAddress());
packet.setRelPort(relAddr.getPort());
}
/*
* FIXME The XML schema of XEP-0176: Jingle ICE-UDP Transport Method
* specifies the network attribute as required.
*/
packet.setNetwork(0);
return packet;
}
/**
* Creates an {@link IceMediaStream} with the specified media
* name.
*
* @param media the name of the stream we'd like to create.
*
* @return the newly created {@link IceMediaStream}
*
* @throws OperationFailedException if binding on the specified media stream
* fails for some reason.
*/
protected IceMediaStream createIceStream(String media)
throws OperationFailedException
{
IceMediaStream stream;
PortTracker portTracker;
try
{
portTracker = getPortTracker(media);
//the following call involves STUN processing so it may take a while
stream
= getNetAddrMgr().createIceStream(
portTracker.getPort(),
media,
iceAgent);
}
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
{
portTracker.setNextPort(
1
+ stream
.getComponent(Component.RTCP)
.getLocalCandidates()
.get(0)
.getTransportAddress()
.getPort());
}
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;
}
/**
* Simply returns the list of local candidates that we gathered during the
* harvest.
*
* @return the list of local candidates that we gathered during the harvest
* @see TransportManagerJabberImpl#wrapupCandidateHarvest()
*/
@Override
public List wrapupCandidateHarvest()
{
return cpeList;
}
/**
* Returns a reference to the {@link NetworkAddressManagerService}. The only
* reason this method exists is that {@link JabberActivator
* #getNetworkAddressManagerService()} is too long to write and makes code
* look clumsy.
*
* @return a reference to the {@link NetworkAddressManagerService}.
*/
private static NetworkAddressManagerService getNetAddrMgr()
{
return JabberActivator.getNetworkAddressManagerService();
}
/**
* Starts the connectivity establishment of the associated ICE
* Agent.
*
* @param remote the collection of ContentPacketExtensions which
* represents the remote counterpart of the negotiation between the local
* and the remote peers
* @return true if connectivity establishment has been started in
* response to the call; otherwise, false
* @see TransportManagerJabberImpl#startConnectivityEstablishment(Iterable)
*/
@Override
public synchronized boolean startConnectivityEstablishment(
Iterable remote)
{
Map map
= new LinkedHashMap();
for (ContentPacketExtension content : remote)
{
IceUdpTransportPacketExtension transport
= content.getFirstChildOfType(
IceUdpTransportPacketExtension.class);
/*
* If we cannot associate an IceMediaStream with the remote content,
* we will not have anything to add the remote candidates to.
*/
RtpDescriptionPacketExtension description
= content.getFirstChildOfType(
RtpDescriptionPacketExtension.class);
if ((description == null) && (cpeList != null))
{
ContentPacketExtension localContent
= findContentByName(cpeList, content.getName());
if (localContent != null)
{
description
= localContent.getFirstChildOfType(
RtpDescriptionPacketExtension.class);
}
}
if (description != null)
{
String media = description.getMedia();
map.put(media, transport);
}
}
/*
* When the local peer is organizing a telephony conference using the
* Jitsi Videobridge server-side technology, it is establishing
* connectivity by using information from a colibri Channel and not from
* the offer/answer of the remote peer.
*/
if (getCallPeer().isJitsiVideobridge())
{
sendTransportInfoToJitsiVideobridge(map);
return false;
}
else
{
return startConnectivityEstablishment(map);
}
}
/**
* Starts the connectivity establishment of the associated ICE
* Agent.
*
* @param remote a Map of
* media-IceUdpTransportPacketExtension pairs which represents the
* remote counterpart of the negotiation between the local and the remote
* peers
* @return true if connectivity establishment has been started in
* response to the call; otherwise, false
* @see TransportManagerJabberImpl#startConnectivityEstablishment(Map)
*/
@Override
protected synchronized boolean startConnectivityEstablishment(
Map remote)
{
/*
* If ICE is running already, we try to update the checklists with the
* candidates. Note that this is a best effort.
*/
boolean iceAgentStateIsRunning
= IceProcessingState.RUNNING.equals(iceAgent.getState());
if (iceAgentStateIsRunning && logger.isInfoEnabled())
logger.info("Update ICE remote candidates");
int generation = iceAgent.getGeneration();
boolean startConnectivityEstablishment = false;
for (Map.Entry e
: remote.entrySet())
{
IceUdpTransportPacketExtension transport = e.getValue();
List candidates
= transport.getChildExtensionsOfType(
CandidatePacketExtension.class);
if (iceAgentStateIsRunning && (candidates.size() == 0))
return false;
String media = e.getKey();
IceMediaStream stream = iceAgent.getStream(media);
if (stream == null)
{
logger.warn(
"No ICE media stream for media: " + media
+ " - ignored candidates.");
continue;
}
// Sort the remote candidates (host < reflexive < relayed) in order
// to create first the host, then the reflexive, the relayed
// candidates and thus be able to set the relative-candidate
// matching the rel-addr/rel-port attribute.
Collections.sort(candidates);
// Different stream may have different ufrag/password
String ufrag = transport.getUfrag();
if (ufrag != null)
stream.setRemoteUfrag(ufrag);
String password = transport.getPassword();
if (password != null)
stream.setRemotePassword(password);
for (CandidatePacketExtension candidate : candidates)
{
/*
* Is the remote candidate from the current generation of the
* iceAgent?
*/
if (candidate.getGeneration() != generation)
continue;
Component component
= stream.getComponent(candidate.getComponent());
String relAddr;
int relPort;
TransportAddress relatedAddress = null;
if (((relAddr = candidate.getRelAddr()) != null)
&& ((relPort = candidate.getRelPort()) != -1))
{
relatedAddress
= new TransportAddress(
relAddr,
relPort,
Transport.parse(candidate.getProtocol()));
}
RemoteCandidate relatedCandidate
= component.findRemoteCandidate(relatedAddress);
RemoteCandidate remoteCandidate
= new RemoteCandidate(
new TransportAddress(
candidate.getIP(),
candidate.getPort(),
Transport.parse(
candidate.getProtocol())),
component,
org.ice4j.ice.CandidateType.parse(
candidate.getType().toString()),
candidate.getFoundation(),
candidate.getPriority(),
relatedCandidate);
if (iceAgentStateIsRunning)
{
component.addUpdateRemoteCandidates(remoteCandidate);
}
else
{
component.addRemoteCandidate(remoteCandidate);
startConnectivityEstablishment = true;
}
}
}
if (iceAgentStateIsRunning)
{
// update all components of all streams
for (IceMediaStream stream : iceAgent.getStreams())
{
for (Component component : stream.getComponents())
component.updateRemoteCandidates();
}
}
else 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 one remote candidate 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 Agent to finish any started
* connectivity checks.
*
* @see TransportManagerJabberImpl#wrapupConnectivityEstablishment()
* @throws OperationFailedException if ICE processing has failed
*/
@Override
public void wrapupConnectivityEstablishment()
throws OperationFailedException
{
TransportManagerJabberImpl delegate
= findTransportManagerEstablishingConnectivityWithJitsiVideobridge();
if ((delegate == null) || (delegate == this))
{
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 == IceUdpTransportManager.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(IceProcessingState.FAILED.equals(iceAgent.getState()))
{
String msg = JabberActivator.getResources()
.getI18NString("service.protocol.ICE_FAILED");
throw new OperationFailedException(
msg,
OperationFailedException.GENERAL_ERROR);
}
}
else
{
delegate.wrapupConnectivityEstablishment();
}
/*
* Once we're done establishing connectivity, we shouldn't be sending
* any more candidates because we will not be able to perform
* connectivity checks for them. Besides, they must have been sent in
* transport-info already.
*/
if (cpeList != null)
{
for (ContentPacketExtension content : cpeList)
{
IceUdpTransportPacketExtension transport
= content.getFirstChildOfType(
IceUdpTransportPacketExtension.class);
if (transport != null)
{
for (CandidatePacketExtension candidate
: transport.getCandidateList())
transport.removeCandidate(candidate);
Collection> childExtensions
= transport.getChildExtensionsOfType(
CandidatePacketExtension.class);
if ((childExtensions == null) || childExtensions.isEmpty())
{
transport.removeAttribute(
IceUdpTransportPacketExtension.UFRAG_ATTR_NAME);
transport.removeAttribute(
IceUdpTransportPacketExtension.PWD_ATTR_NAME);
}
}
}
}
}
/**
* Removes a content with a specific name from the transport-related part of
* the session represented by this TransportManagerJabberImpl which
* may have been reported through previous calls to the
* startCandidateHarvest and
* startConnectivityEstablishment methods.
*
* @param name the name of the content to be removed from the
* transport-related part of the session represented by this
* TransportManagerJabberImpl
* @see TransportManagerJabberImpl#removeContent(String)
*/
@Override
public void removeContent(String name)
{
ContentPacketExtension content = removeContent(cpeList, name);
if (content != null)
{
RtpDescriptionPacketExtension rtpDescription
= content.getFirstChildOfType(
RtpDescriptionPacketExtension.class);
if (rtpDescription != null)
{
IceMediaStream stream
= iceAgent.getStream(rtpDescription.getMedia());
if (stream != null)
iceAgent.removeStream(stream);
}
}
}
/**
* Close this transport manager and release resources. In case of ICE, it
* releases Ice4j's Agent that will cleanup all streams, component and close
* every candidate's sockets.
*/
@Override
public synchronized void close()
{
if(iceAgent != null)
{
iceAgent.removeStateChangeListener(this);
iceAgent.free();
}
}
/**
* Returns the extended type of the candidate selected if this transport
* manager is using ICE.
*
* @param streamName The stream name (AUDIO, VIDEO);
*
* @return The extended type of the candidate selected if this transport
* manager is using ICE. Otherwise, returns null.
*/
@Override
public String getICECandidateExtendedType(String streamName)
{
return
TransportManager.getICECandidateExtendedType(iceAgent, streamName);
}
/**
* Returns the current state of ICE processing.
*
* @return the current state of ICE processing.
*/
@Override
public String getICEState()
{
return iceAgent.getState().toString();
}
/**
* Returns the ICE local host address.
*
* @param streamName The stream name (AUDIO, VIDEO);
*
* @return the ICE local host address if this transport
* manager is using ICE. Otherwise, returns null.
*/
@Override
public InetSocketAddress getICELocalHostAddress(String streamName)
{
if(iceAgent != null)
{
LocalCandidate localCandidate
= iceAgent.getSelectedLocalCandidate(streamName);
if(localCandidate != null)
return localCandidate.getHostAddress();
}
return null;
}
/**
* Returns the ICE remote host address.
*
* @param streamName The stream name (AUDIO, VIDEO);
*
* @return the ICE remote host address if this transport
* manager is using ICE. Otherwise, returns null.
*/
@Override
public InetSocketAddress getICERemoteHostAddress(String streamName)
{
if(iceAgent != null)
{
RemoteCandidate remoteCandidate
= iceAgent.getSelectedRemoteCandidate(streamName);
if(remoteCandidate != null)
return remoteCandidate.getHostAddress();
}
return null;
}
/**
* Returns the ICE local reflexive address (server or peer reflexive).
*
* @param streamName The stream name (AUDIO, VIDEO);
*
* @return the ICE local reflexive address. May be null if this transport
* manager is not using ICE or if there is no reflexive address for the
* local candidate used.
*/
@Override
public InetSocketAddress getICELocalReflexiveAddress(String streamName)
{
if(iceAgent != null)
{
LocalCandidate localCandidate
= iceAgent.getSelectedLocalCandidate(streamName);
if(localCandidate != null)
return localCandidate.getReflexiveAddress();
}
return null;
}
/**
* Returns the ICE remote reflexive address (server or peer reflexive).
*
* @param streamName The stream name (AUDIO, VIDEO);
*
* @return the ICE remote reflexive address. May be null if this transport
* manager is not using ICE or if there is no reflexive address for the
* remote candidate used.
*/
@Override
public InetSocketAddress getICERemoteReflexiveAddress(String streamName)
{
if(iceAgent != null)
{
RemoteCandidate remoteCandidate
= iceAgent.getSelectedRemoteCandidate(streamName);
if(remoteCandidate != null)
return remoteCandidate.getReflexiveAddress();
}
return null;
}
/**
* Returns the ICE local relayed address (server or peer relayed).
*
* @param streamName The stream name (AUDIO, VIDEO);
*
* @return the ICE local relayed address. May be null if this transport
* manager is not using ICE or if there is no relayed address for the
* local candidate used.
*/
@Override
public InetSocketAddress getICELocalRelayedAddress(String streamName)
{
if(iceAgent != null)
{
LocalCandidate localCandidate
= iceAgent.getSelectedLocalCandidate(streamName);
if(localCandidate != null)
return localCandidate.getRelayedAddress();
}
return null;
}
/**
* Returns the ICE remote relayed address (server or peer relayed).
*
* @param streamName The stream name (AUDIO, VIDEO);
*
* @return the ICE remote relayed address. May be null if this transport
* manager is not using ICE or if there is no relayed address for the
* remote candidate used.
*/
@Override
public InetSocketAddress getICERemoteRelayedAddress(String streamName)
{
if(iceAgent != null)
{
RemoteCandidate remoteCandidate
= iceAgent.getSelectedRemoteCandidate(streamName);
if(remoteCandidate != null)
return remoteCandidate.getRelayedAddress();
}
return null;
}
/**
* Returns the total harvesting time (in ms) for all harvesters.
*
* @return The total harvesting time (in ms) for all the harvesters. 0 if
* the ICE agent is null, or if the agent has nevers harvested.
*/
@Override
public long getTotalHarvestingTime()
{
return (iceAgent == null) ? 0 : iceAgent.getTotalHarvestingTime();
}
/**
* Returns the harvesting time (in ms) for the harvester given in parameter.
*
* @param harvesterName The class name if the harvester.
*
* @return The harvesting time (in ms) for the harvester given in parameter.
* 0 if this harvester does not exists, if the ICE agent is null, or if the
* agent has never harvested with this harvester.
*/
@Override
public long getHarvestingTime(String harvesterName)
{
return
(iceAgent == null) ? 0 : iceAgent.getHarvestingTime(harvesterName);
}
/**
* Returns the number of harvesting for this agent.
*
* @return The number of harvesting for this agent.
*/
@Override
public int getNbHarvesting()
{
return (iceAgent == null) ? 0 : iceAgent.getHarvestCount();
}
/**
* Returns the number of harvesting time for the harvester given in
* parameter.
*
* @param harvesterName The class name if the harvester.
*
* @return The number of harvesting time for the harvester given in
* parameter.
*/
public int getNbHarvesting(String harvesterName)
{
return (iceAgent == null) ? 0 : iceAgent.getHarvestCount(harvesterName);
}
/**
* Retransmit state change events from the Agent to the media handler.
* @param evt the event for state change.
*/
public void propertyChange(PropertyChangeEvent evt)
{
getCallPeer().getMediaHandler().firePropertyChange(
evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
}
}