From 979d99145ee25600a548a05e4c572b68a1a5a3ae Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Tue, 6 May 2014 16:01:44 +0200 Subject: [PATCH] Localize ICE failed message and add "no relay found" --- resources/languages/resources.properties | 2 + .../jabber/IceUdpTransportManager.java | 2812 +++++++++-------- 2 files changed, 1409 insertions(+), 1405 deletions(-) diff --git a/resources/languages/resources.properties b/resources/languages/resources.properties index 427174302..32150bf70 100644 --- a/resources/languages/resources.properties +++ b/resources/languages/resources.properties @@ -805,6 +805,8 @@ service.gui.avatar.imagepicker.WEBCAM_ERROR=Webcam error service.gui.security.encryption.required=Encryption required! +service.protocol.ICE_FAILED=Could not establish connection (ICE failed and no relay found) + # impl.protocol.ssh #Contact Details Seperator(must not be part of contact data stored as persistent # data) diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/IceUdpTransportManager.java b/src/net/java/sip/communicator/impl/protocol/jabber/IceUdpTransportManager.java index d3280cbf5..73f1b0b28 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/IceUdpTransportManager.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/IceUdpTransportManager.java @@ -1,1405 +1,1407 @@ -/* - * Jitsi, 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.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; - - // 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); - - String media = e.getKey(); - IceMediaStream stream = iceAgent.getStream(media); - - // 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())) - { - throw new OperationFailedException( - "Could not establish connection (ICE failed)", - 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()); - } -} +/* + * Jitsi, 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.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; + + // 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); + + String media = e.getKey(); + IceMediaStream stream = iceAgent.getStream(media); + + // 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()); + } +}