diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/CallJabberImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/CallJabberImpl.java index 03df31660..e0955cfb7 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/CallJabberImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/CallJabberImpl.java @@ -1,1381 +1,1409 @@ -/* - * 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.lang.ref.*; -import java.util.*; - -import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; -import net.java.sip.communicator.impl.protocol.jabber.jinglesdp.*; -import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.service.protocol.event.*; -import net.java.sip.communicator.service.protocol.media.*; -import net.java.sip.communicator.util.*; - -import org.jitsi.service.neomedia.*; -import org.jivesoftware.smack.*; -import org.jivesoftware.smack.filter.*; -import org.jivesoftware.smack.packet.*; -import org.jivesoftware.smackx.packet.*; - -/** - * A Jabber implementation of the Call abstract class encapsulating - * Jabber jingle sessions. - * - * @author Emil Ivov - * @author Lyubomir Marinov - * @author Boris Grozev - */ -public class CallJabberImpl - extends AbstractCallJabberGTalkImpl -{ - /** - * The Logger used by the CallJabberImpl class and its - * instances for logging output. - */ - private static final Logger logger = Logger.getLogger(CallJabberImpl.class); - - /** - * The Jitsi Videobridge conference which the local peer represented by this - * instance is a focus of. - */ - private ColibriConferenceIQ colibri; - - /** - * The shared CallPeerMediaHandler state which is to be used by the - * CallPeers of this Call which use {@link #colibri}. - */ - private MediaHandler colibriMediaHandler; - - /** - * Contains one ColibriStreamConnector for each MediaType - */ - private final List> - colibriStreamConnectors; - - /** - * Initializes a new CallJabberImpl instance. - * - * @param parentOpSet the {@link OperationSetBasicTelephonyJabberImpl} - * instance in the context of which this call has been created. - */ - protected CallJabberImpl( - OperationSetBasicTelephonyJabberImpl parentOpSet) - { - super(parentOpSet); - - int mediaTypeValueCount = MediaType.values().length; - - colibriStreamConnectors - = new ArrayList>( - mediaTypeValueCount); - for (int i = 0; i < mediaTypeValueCount; i++) - colibriStreamConnectors.add(null); - - //let's add ourselves to the calls repo. we are doing it ourselves just - //to make sure that no one ever forgets. - parentOpSet.getActiveCallsRepository().addCall(this); - } - - /** - * Closes a specific ColibriStreamConnector which is associated with - * a MediaStream of a specific MediaType upon request from - * a specific CallPeer. - * - * @param peer the CallPeer which requests the closing of the - * specified colibriStreamConnector - * @param mediaType the MediaType of the MediaStream with - * which the specified colibriStreamConnector is associated - * @param colibriStreamConnector the ColibriStreamConnector to close on - * behalf of the specified peer - */ - public void closeColibriStreamConnector( - CallPeerJabberImpl peer, - MediaType mediaType, - ColibriStreamConnector colibriStreamConnector) - { - colibriStreamConnector.close(); - synchronized (colibriStreamConnectors) - { - int index = mediaType.ordinal(); - WeakReference weakReference - = colibriStreamConnectors.get(index); - if (weakReference != null && colibriStreamConnector - .equals(weakReference.get())) - { - colibriStreamConnectors.set(index, null); - } - } - } - - /** - * {@inheritDoc} - * - * Sends a content message to each of the CallPeers - * associated with this CallJabberImpl in order to include/exclude - * the "isfocus" attribute. - */ - @Override - protected void conferenceFocusChanged(boolean oldValue, boolean newValue) - { - try - { - Iterator peers = getCallPeers(); - - while (peers.hasNext()) - { - CallPeerJabberImpl callPeer = peers.next(); - - if (callPeer.getState() == CallPeerState.CONNECTED) - callPeer.sendCoinSessionInfo(); - } - } - finally - { - super.conferenceFocusChanged(oldValue, newValue); - } - } - - /** - * Allocates colibri (conference) channels for a specific MediaType - * to be used by a specific CallPeer. - * - * @param peer the CallPeer which is to use the allocated colibri - * (conference) channels - * @param contentMap the local and remote ContentPacketExtensions - * which specify the MediaTypes for which colibri (conference) - * channels are to be allocated - * @return a ColibriConferenceIQ which describes the allocated - * colibri (conference) channels for the specified mediaTypes which - * are to be used by the specified peer; otherwise, null - */ - public ColibriConferenceIQ createColibriChannels( - CallPeerJabberImpl peer, - Map contentMap) - throws OperationFailedException - { - if (!getConference().isJitsiVideobridge()) - return null; - - /* - * For a colibri conference to work properly, all CallPeers in the - * conference must share one and the same CallPeerMediaHandler state - * i.e. they must use a single set of MediaStreams as if there was a - * single CallPeerMediaHandler. - */ - CallPeerMediaHandlerJabberImpl peerMediaHandler - = peer.getMediaHandler(); - - if (peerMediaHandler.getMediaHandler() != colibriMediaHandler) - { - for (MediaType mediaType : MediaType.values()) - { - if (peerMediaHandler.getStream(mediaType) != null) - return null; - } - } - - ProtocolProviderServiceJabberImpl protocolProvider - = getProtocolProvider(); - String jitsiVideobridge - = (colibri == null) - ? protocolProvider.getJitsiVideobridge() - : colibri.getFrom(); - - if ((jitsiVideobridge == null) || (jitsiVideobridge.length() == 0)) - { - logger.error( - "Failed to allocate colibri channels: no videobridge" - + " found."); - return null; - } - - /* - * The specified CallPeer will participate in the colibri conference - * organized by this Call so it must use the shared CallPeerMediaHandler - * state of all CallPeers in the same colibri conference. - */ - if (colibriMediaHandler == null) - colibriMediaHandler = new MediaHandler(); - peerMediaHandler.setMediaHandler(colibriMediaHandler); - - ColibriConferenceIQ conferenceRequest = new ColibriConferenceIQ(); - - if (colibri != null) - conferenceRequest.setID(colibri.getID()); - - for (Map.Entry e - : contentMap.entrySet()) - { - ContentPacketExtension localContent = e.getKey(); - ContentPacketExtension remoteContent = e.getValue(); - ContentPacketExtension cpe - = (remoteContent == null) ? localContent : remoteContent; - RtpDescriptionPacketExtension rdpe - = cpe.getFirstChildOfType( - RtpDescriptionPacketExtension.class); - String media = rdpe.getMedia(); - MediaType mediaType = MediaType.parseString(media); - String contentName = mediaType.toString(); - ColibriConferenceIQ.Content contentRequest - = new ColibriConferenceIQ.Content(contentName); - - conferenceRequest.addContent(contentRequest); - - boolean requestLocalChannel = true; - - if (colibri != null) - { - ColibriConferenceIQ.Content content - = colibri.getContent(contentName); - - if ((content != null) && (content.getChannelCount() > 0)) - requestLocalChannel = false; - } - - boolean peerIsInitiator = peer.isInitiator(); - - if (requestLocalChannel) - { - ColibriConferenceIQ.Channel localChannelRequest - = new ColibriConferenceIQ.Channel(); - - localChannelRequest.setInitiator(peerIsInitiator); - - for (PayloadTypePacketExtension ptpe : rdpe.getPayloadTypes()) - localChannelRequest.addPayloadType(ptpe); - setTransportOnChannel(peer, media, localChannelRequest); - // DTLS-SRTP - setDtlsEncryptionOnChannel( - jitsiVideobridge, - peer, - mediaType, - localChannelRequest); - /* - * Since Jitsi Videobridge supports multiple Jingle transports, - * it is a good idea to indicate which one is expected on a - * channel. - */ - ensureTransportOnChannel(localChannelRequest, peer); - contentRequest.addChannel(localChannelRequest); - } - - ColibriConferenceIQ.Channel remoteChannelRequest - = new ColibriConferenceIQ.Channel(); - - remoteChannelRequest.setInitiator(!peerIsInitiator); - - for (PayloadTypePacketExtension ptpe : rdpe.getPayloadTypes()) - remoteChannelRequest.addPayloadType(ptpe); - setTransportOnChannel( - media, - localContent, - remoteContent, - peer, - remoteChannelRequest); - // DTLS-SRTP - setDtlsEncryptionOnChannel( - mediaType, - localContent, - remoteContent, - peer, - remoteChannelRequest); - /* - * Since Jitsi Videobridge supports multiple Jingle transports, it - * is a good idea to indicate which one is expected on a channel. - */ - ensureTransportOnChannel(remoteChannelRequest, peer); - contentRequest.addChannel(remoteChannelRequest); - } - - XMPPConnection connection = protocolProvider.getConnection(); - PacketCollector packetCollector - = connection.createPacketCollector( - new PacketIDFilter(conferenceRequest.getPacketID())); - - conferenceRequest.setTo(jitsiVideobridge); - conferenceRequest.setType(IQ.Type.GET); - connection.sendPacket(conferenceRequest); - - Packet response - = packetCollector.nextResult( - SmackConfiguration.getPacketReplyTimeout()); - - packetCollector.cancel(); - - if (response == null) - { - logger.error( - "Failed to allocate colibri channels: response is null." - + " Maybe the response timed out."); - return null; - } - else if (response.getError() != null) - { - logger.error( - "Failed to allocate colibri channels: " - + response.getError()); - return null; - } - else if (!(response instanceof ColibriConferenceIQ)) - { - logger.error( - "Failed to allocate colibri channels: response is not a" - + " colibri conference"); - return null; - } - - ColibriConferenceIQ conferenceResponse = (ColibriConferenceIQ) response; - String conferenceResponseID = conferenceResponse.getID(); - - /* - * Update the complete ColibriConferenceIQ representation maintained by - * this instance with the information given by the (current) response. - */ - { - if (colibri == null) - { - colibri = new ColibriConferenceIQ(); - /* - * XXX We must remember the JID of the Jitsi Videobridge because - * (1) we do not want to re-discover it in every method - * invocation on this Call instance and (2) we want to use one - * and the same for all CallPeers within this Call instance. - */ - colibri.setFrom(conferenceResponse.getFrom()); - } - - String colibriID = colibri.getID(); - - if (colibriID == null) - colibri.setID(conferenceResponseID); - else if (!colibriID.equals(conferenceResponseID)) - throw new IllegalStateException("conference.id"); - - for (ColibriConferenceIQ.Content contentResponse - : conferenceResponse.getContents()) - { - String contentName = contentResponse.getName(); - ColibriConferenceIQ.Content content - = colibri.getOrCreateContent(contentName); - - for (ColibriConferenceIQ.Channel channelResponse - : contentResponse.getChannels()) - { - int channelIndex = content.getChannelCount(); - - content.addChannel(channelResponse); - if (channelIndex == 0) - { - TransportManagerJabberImpl transportManager - = peerMediaHandler.getTransportManager(); - - transportManager - .isEstablishingConnectivityWithJitsiVideobridge - = true; - transportManager - .startConnectivityEstablishmentWithJitsiVideobridge - = true; - - MediaType mediaType - = MediaType.parseString(contentName); - - // DTLS-SRTP - addDtlsAdvertisedEncryptions( - peer, - channelResponse, - mediaType); - } - } - } - } - - /* - * Formulate the result to be returned to the caller which is a subset - * of the whole conference information kept by this CallJabberImpl and - * includes the remote channels explicitly requested by the method - * caller and their respective local channels. - */ - ColibriConferenceIQ conferenceResult = new ColibriConferenceIQ(); - - conferenceResult.setFrom(colibri.getFrom()); - conferenceResult.setID(conferenceResponseID); - - for (Map.Entry e - : contentMap.entrySet()) - { - ContentPacketExtension localContent = e.getKey(); - ContentPacketExtension remoteContent = e.getValue(); - ContentPacketExtension cpe - = (remoteContent == null) ? localContent : remoteContent; - RtpDescriptionPacketExtension rdpe - = cpe.getFirstChildOfType( - RtpDescriptionPacketExtension.class); - MediaType mediaType = MediaType.parseString(rdpe.getMedia()); - ColibriConferenceIQ.Content contentResponse - = conferenceResponse.getContent(mediaType.toString()); - - if (contentResponse != null) - { - String contentName = contentResponse.getName(); - ColibriConferenceIQ.Content contentResult - = new ColibriConferenceIQ.Content(contentName); - - conferenceResult.addContent(contentResult); - - /* - * The local channel may have been allocated in a previous - * method call as part of the allocation of the first remote - * channel in the respective content. Anyway, the current method - * caller still needs to know about it. - */ - ColibriConferenceIQ.Content content - = colibri.getContent(contentName); - ColibriConferenceIQ.Channel localChannel = null; - - if ((content != null) && (content.getChannelCount() > 0)) - { - localChannel = content.getChannel(0); - contentResult.addChannel(localChannel); - } - - String localChannelID - = (localChannel == null) ? null : localChannel.getID(); - - for (ColibriConferenceIQ.Channel channelResponse - : contentResponse.getChannels()) - { - if ((localChannelID == null) - || !localChannelID.equals(channelResponse.getID())) - contentResult.addChannel(channelResponse); - } - } - } - - return conferenceResult; - } - - /** - * Initializes a ColibriStreamConnector on behalf of a specific - * CallPeer to be used in association with a specific - * ColibriConferenceIQ.Channel of a specific MediaType. - * - * @param peer the CallPeer which requests the initialization of a - * ColibriStreamConnector - * @param mediaType the MediaType of the stream which is to use the - * initialized ColibriStreamConnector for RTP and RTCP traffic - * @param channel the ColibriConferenceIQ.Channel to which RTP and - * RTCP traffic is to be sent and from which such traffic is to be received - * via the initialized ColibriStreamConnector - * @param factory a StreamConnectorFactory implementation which is - * to allocate the sockets to be used for RTP and RTCP traffic - * @return a ColibriStreamConnector to be used for RTP and RTCP - * traffic associated with the specified channel - */ - public ColibriStreamConnector createColibriStreamConnector( - CallPeerJabberImpl peer, - MediaType mediaType, - ColibriConferenceIQ.Channel channel, - StreamConnectorFactory factory) - { - String channelID = channel.getID(); - - if (channelID == null) - throw new IllegalArgumentException("channel"); - - if (colibri == null) - throw new IllegalStateException("colibri"); - - ColibriConferenceIQ.Content content - = colibri.getContent(mediaType.toString()); - - if (content == null) - throw new IllegalArgumentException("mediaType"); - if ((content.getChannelCount() < 1) - || !channelID.equals((channel = content.getChannel(0)).getID())) - throw new IllegalArgumentException("channel"); - - ColibriStreamConnector colibriStreamConnector; - - synchronized (colibriStreamConnectors) - { - int index = mediaType.ordinal(); - WeakReference weakReference - = colibriStreamConnectors.get(index); - - colibriStreamConnector - = (weakReference == null) ? null : weakReference.get(); - if (colibriStreamConnector == null) - { - StreamConnector streamConnector - = factory.createStreamConnector(); - - if (streamConnector != null) - { - colibriStreamConnector - = new ColibriStreamConnector(streamConnector); - colibriStreamConnectors.set( - index, - new WeakReference( - colibriStreamConnector)); - } - } - } - - return colibriStreamConnector; - } - - /** - * Expires specific (colibri) conference channels used by a specific - * CallPeer. - * - * @param peer the CallPeer which uses the specified (colibri) - * conference channels to be expired - * @param conference a ColibriConferenceIQ which specifies the - * (colibri) conference channels to be expired - */ - public void expireColibriChannels( - CallPeerJabberImpl peer, - ColibriConferenceIQ conference) - { - // Formulate the ColibriConferenceIQ request which is to be sent. - if (colibri != null) - { - String conferenceID = colibri.getID(); - - if (conferenceID.equals(conference.getID())) - { - ColibriConferenceIQ conferenceRequest - = new ColibriConferenceIQ(); - - conferenceRequest.setID(conferenceID); - - for (ColibriConferenceIQ.Content content - : conference.getContents()) - { - ColibriConferenceIQ.Content colibriContent - = colibri.getContent(content.getName()); - - if (colibriContent != null) - { - ColibriConferenceIQ.Content contentRequest - = conferenceRequest.getOrCreateContent( - colibriContent.getName()); - - for (ColibriConferenceIQ.Channel channel - : content.getChannels()) - { - ColibriConferenceIQ.Channel colibriChannel - = colibriContent.getChannel(channel.getID()); - - if (colibriChannel != null) - { - ColibriConferenceIQ.Channel channelRequest - = new ColibriConferenceIQ.Channel(); - - channelRequest.setExpire(0); - channelRequest.setID(colibriChannel.getID()); - contentRequest.addChannel(channelRequest); - } - } - } - } - - /* - * Remove the channels which are to be expired from the internal - * state of the conference managed by this CallJabberImpl. - */ - for (ColibriConferenceIQ.Content contentRequest - : conferenceRequest.getContents()) - { - ColibriConferenceIQ.Content colibriContent - = colibri.getContent(contentRequest.getName()); - - for (ColibriConferenceIQ.Channel channelRequest - : contentRequest.getChannels()) - { - ColibriConferenceIQ.Channel colibriChannel - = colibriContent.getChannel(channelRequest.getID()); - - colibriContent.removeChannel(colibriChannel); - - /* - * If the last remote channel is to be expired, expire - * the local channel as well. - */ - if (colibriContent.getChannelCount() == 1) - { - colibriChannel = colibriContent.getChannel(0); - - channelRequest = new ColibriConferenceIQ.Channel(); - channelRequest.setExpire(0); - channelRequest.setID(colibriChannel.getID()); - contentRequest.addChannel(channelRequest); - - colibriContent.removeChannel(colibriChannel); - - break; - } - } - } - - /* - * At long last, send the ColibriConferenceIQ request to expire - * the channels. - */ - conferenceRequest.setTo(colibri.getFrom()); - conferenceRequest.setType(IQ.Type.SET); - getProtocolProvider().getConnection().sendPacket( - conferenceRequest); - } - } - } - - /** - * Sends a ColibriConferenceIQ to the videobridge used by this - * CallJabberImpl, in order to request the the direction of - * the channel with ID channelID be set to - * direction - * @param channelID the ID of the channel for which to set the - * direction. - * @param mediaType the MediaType of the channel (we can deduce this - * by searching the ColibriConferenceIQ, but it's more convenient - * to have it) - * @param direction the MediaDirection to set. - */ - public void setChannelDirection(String channelID, - MediaType mediaType, - MediaDirection direction) - { - if ((colibri != null) && (channelID != null)) - { - ColibriConferenceIQ.Content content - = colibri.getContent(mediaType.toString()); - - if (content != null) - { - ColibriConferenceIQ.Channel channel - = content.getChannel(channelID); - - /* - * Note that we send requests even when the local Channel's - * direction and the direction we are setting are the same. We - * can easily avoid this, but we risk not sending necessary - * packets if local Channel and the actual channel on the - * videobridge are out of sync. - */ - if (channel != null) - { - ColibriConferenceIQ.Channel requestChannel - = new ColibriConferenceIQ.Channel(); - - requestChannel.setID(channelID); - requestChannel.setDirection(direction); - - ColibriConferenceIQ.Content requestContent - = new ColibriConferenceIQ.Content(); - - requestContent.setName(mediaType.toString()); - requestContent.addChannel(requestChannel); - - ColibriConferenceIQ conferenceRequest - = new ColibriConferenceIQ(); - - conferenceRequest.setID(colibri.getID()); - conferenceRequest.setTo(colibri.getFrom()); - conferenceRequest.setType(IQ.Type.SET); - conferenceRequest.addContent(requestContent); - - getProtocolProvider().getConnection().sendPacket( - conferenceRequest); - } - } - } - } - - /** - * Creates a CallPeerJabberImpl from calleeJID and sends - * them session-initiate IQ request. - * - * @param calleeJID the party that we would like to invite to this call. - * @param discoverInfo any discovery information that we have for the jid - * we are trying to reach and that we are passing in order to avoid having - * to ask for it again. - * @param sessionInitiateExtensions a collection of additional and optional - * PacketExtensions to be added to the session-initiate - * {@link JingleIQ} which is to init this CallJabberImpl - * @param supportedTransports the XML namespaces of the jingle transports - * to use. - * - * @return the newly created CallPeerJabberImpl corresponding to - * calleeJID. All following state change events will be - * delivered through this call peer. - * - * @throws OperationFailedException with the corresponding code if we fail - * to create the call. - */ - public CallPeerJabberImpl initiateSession( - String calleeJID, - DiscoverInfo discoverInfo, - Iterable sessionInitiateExtensions, - Collection supportedTransports) - throws OperationFailedException - { - // create the session-initiate IQ - CallPeerJabberImpl callPeer = new CallPeerJabberImpl(calleeJID, this); - - callPeer.setDiscoveryInfo(discoverInfo); - - addCallPeer(callPeer); - - callPeer.setState(CallPeerState.INITIATING_CALL); - - // If this was the first peer we added in this call, then the call is - // new and we need to notify everyone of its creation. - if (getCallPeerCount() == 1) - parentOpSet.fireCallEvent(CallEvent.CALL_INITIATED, this); - - CallPeerMediaHandlerJabberImpl mediaHandler - = callPeer.getMediaHandler(); - - //set the supported transports before the transport manager is created - mediaHandler.setSupportedTransports(supportedTransports); - - /* enable video if it is a video call */ - mediaHandler.setLocalVideoTransmissionEnabled(localVideoAllowed); - /* enable remote-control if it is a desktop sharing session */ - mediaHandler.setLocalInputEvtAware(getLocalInputEvtAware()); - - /* - * Set call state to connecting so that the user interface would start - * playing the tones. We do that here because we may be harvesting - * STUN/TURN addresses in initiateSession() which would take a while. - */ - callPeer.setState(CallPeerState.CONNECTING); - - // if initializing session fails, set peer to failed - boolean sessionInitiated = false; - - try - { - callPeer.initiateSession(sessionInitiateExtensions); - sessionInitiated = true; - } - finally - { - // if initialization throws an exception - if (!sessionInitiated) - callPeer.setState(CallPeerState.FAILED); - } - return callPeer; - } - - /** - * Updates the Jingle sessions for the CallPeers of this - * Call, to reflect the current state of the the video contents of - * this Call. Sends a content-modify, content-add - * or content-remove message to each of the current - * CallPeers. - * - * @throws OperationFailedException if a problem occurred during message - * generation or there was a network problem - */ - @Override - public void modifyVideoContent() - throws OperationFailedException - { - if (logger.isDebugEnabled()) - logger.debug("Updating video content for " + this); - - boolean change = false; - for (CallPeerJabberImpl peer : getCallPeerList()) - change |= peer.sendModifyVideoContent(); - - if (change) - fireCallChangeEvent( - CallChangeEvent.CALL_PARTICIPANTS_CHANGE, null, null); - } - - /** - * Notifies this instance that a specific ColibriConferenceIQ has - * been received. - * - * @param conferenceIQ the ColibriConferenceIQ which has been - * received - * @return true if the specified conferenceIQ was - * processed by this instance and no further processing is to be performed - * by other possible processors of ColibriConferenceIQs; otherwise, - * false. Because a ColibriConferenceIQ request sent from - * the Jitsi Videobridge server to the application as its client concerns a - * specific CallJabberImpl implementation, no further processing by - * other CallJabberImpl instances is necessary once the - * ColibriConferenceIQ is processed by the associated - * CallJabberImpl instance. - */ - boolean processColibriConferenceIQ(ColibriConferenceIQ conferenceIQ) - { - if (colibri == null) - { - /* - * This instance has not set up any conference using the Jitsi - * Videobridge server-side technology yet so it cannot be bothered - * with related requests. - */ - return false; - } - else if (conferenceIQ.getID().equals(colibri.getID())) - { - /* - * Remove the local Channels (from the specified conferenceIQ) i.e. - * the Channels on which the local peer/user is sending to the Jitsi - * Videobridge server because they concern this Call only and not - * its CallPeers. - */ - for (MediaType mediaType : MediaType.values()) - { - String contentName = mediaType.toString(); - ColibriConferenceIQ.Content content - = conferenceIQ.getContent(contentName); - - if (content != null) - { - ColibriConferenceIQ.Content thisContent - = colibri.getContent(contentName); - - if ((thisContent != null) - && (thisContent.getChannelCount() > 0)) - { - ColibriConferenceIQ.Channel thisChannel - = thisContent.getChannel(0); - ColibriConferenceIQ.Channel channel - = content.getChannel(thisChannel.getID()); - - if (channel != null) - content.removeChannel(channel); - } - } - } - - for (CallPeerJabberImpl callPeer : getCallPeerList()) - callPeer.processColibriConferenceIQ(conferenceIQ); - - /* - * We have removed the local Channels from the specified - * conferenceIQ. Consequently, it is no longer the same and fit for - * processing by other CallJabberImpl instances. - */ - return true; - } - else - { - /* - * This instance has set up a conference using the Jitsi Videobridge - * server-side technology but it is not the one referred to by the - * specified conferenceIQ i.e. the specified conferenceIQ does not - * concern this instance. - */ - return false; - } - } - - /** - * Creates a new call peer and sends a RINGING response. - * - * @param jingleIQ the {@link JingleIQ} that created the session. - * - * @return the newly created {@link CallPeerJabberImpl} (the one that sent - * the INVITE). - */ - public CallPeerJabberImpl processSessionInitiate(JingleIQ jingleIQ) - { - String remoteParty = jingleIQ.getInitiator(); - boolean autoAnswer = false; - CallPeerJabberImpl attendant = null; - OperationSetBasicTelephonyJabberImpl basicTelephony = null; - - //according to the Jingle spec initiator may be null. - if (remoteParty == null) - remoteParty = jingleIQ.getFrom(); - - CallPeerJabberImpl callPeer - = new CallPeerJabberImpl(remoteParty, this, jingleIQ); - - addCallPeer(callPeer); - - /* - * We've already sent ack to the specified session-initiate so if it has - * been sent as part of an attended transfer, we have to hang up on the - * attendant. - */ - try - { - TransferPacketExtension transfer - = (TransferPacketExtension) - jingleIQ.getExtension( - TransferPacketExtension.ELEMENT_NAME, - TransferPacketExtension.NAMESPACE); - - if (transfer != null) - { - String sid = transfer.getSID(); - - if (sid != null) - { - ProtocolProviderServiceJabberImpl protocolProvider - = getProtocolProvider(); - basicTelephony - = (OperationSetBasicTelephonyJabberImpl) - protocolProvider.getOperationSet( - OperationSetBasicTelephony.class); - CallJabberImpl attendantCall - = basicTelephony - .getActiveCallsRepository() - .findSID(sid); - - if (attendantCall != null) - { - attendant = attendantCall.getPeer(sid); - if ((attendant != null) - && basicTelephony - .getFullCalleeURI(attendant.getAddress()) - .equals(transfer.getFrom()) - && protocolProvider.getOurJID().equals( - transfer.getTo())) - { - //basicTelephony.hangupCallPeer(attendant); - autoAnswer = true; - } - } - } - } - } - catch (Throwable t) - { - logger.error( - "Failed to hang up on attendant" - + " as part of session transfer", - t); - - if (t instanceof ThreadDeath) - throw (ThreadDeath) t; - } - - CoinPacketExtension coin - = (CoinPacketExtension) - jingleIQ.getExtension( - CoinPacketExtension.ELEMENT_NAME, - CoinPacketExtension.NAMESPACE); - - if (coin != null) - { - boolean b - = Boolean.parseBoolean( - (String) - coin.getAttribute( - CoinPacketExtension.ISFOCUS_ATTR_NAME)); - - callPeer.setConferenceFocus(b); - } - - //before notifying about this call, make sure that it looks alright - callPeer.processSessionInitiate(jingleIQ); - - // if paranoia is set, to accept the call we need to know that - // the other party has support for media encryption - if (getProtocolProvider().getAccountID().getAccountPropertyBoolean( - ProtocolProviderFactory.MODE_PARANOIA, false) - && callPeer.getMediaHandler().getAdvertisedEncryptionMethods() - .length - == 0) - { - //send an error response; - String reasonText - = JabberActivator.getResources().getI18NString( - "service.gui.security.encryption.required"); - JingleIQ errResp - = JinglePacketFactory.createSessionTerminate( - jingleIQ.getTo(), - jingleIQ.getFrom(), - jingleIQ.getSID(), - Reason.SECURITY_ERROR, - reasonText); - - callPeer.setState(CallPeerState.FAILED, reasonText); - getProtocolProvider().getConnection().sendPacket(errResp); - - return null; - } - - if (callPeer.getState() == CallPeerState.FAILED) - return null; - - callPeer.setState( CallPeerState.INCOMING_CALL ); - - // in case of attended transfer, auto answer the call - if (autoAnswer) - { - /* answer directly */ - try - { - callPeer.answer(); - } - catch(Exception e) - { - logger.info( - "Exception occurred while answer transferred call", - e); - callPeer = null; - } - - // hang up now - try - { - basicTelephony.hangupCallPeer(attendant); - } - catch(OperationFailedException e) - { - logger.error( - "Failed to hang up on attendant as part of session" - + " transfer", - e); - } - - return callPeer; - } - - /* see if offer contains audio and video so that we can propose - * option to the user (i.e. answer with video if it is a video call...) - */ - List offer - = callPeer.getSessionIQ().getContentList(); - Map directions - = new HashMap(); - - directions.put(MediaType.AUDIO, MediaDirection.INACTIVE); - directions.put(MediaType.VIDEO, MediaDirection.INACTIVE); - - for (ContentPacketExtension c : offer) - { - String contentName = c.getName(); - MediaDirection remoteDirection - = JingleUtils.getDirection(c, callPeer.isInitiator()); - - if (MediaType.AUDIO.toString().equals(contentName)) - directions.put(MediaType.AUDIO, remoteDirection); - else if (MediaType.VIDEO.toString().equals(contentName)) - directions.put(MediaType.VIDEO, remoteDirection); - } - - // If this was the first peer we added in this call, then the call is - // new and we need to notify everyone of its creation. - if (getCallPeerCount() == 1) - { - parentOpSet.fireCallEvent( - CallEvent.CALL_RECEIVED, - this, - directions); - } - - // Manages auto answer with "audio only", or "audio/video" answer. - OperationSetAutoAnswerJabberImpl autoAnswerOpSet - = (OperationSetAutoAnswerJabberImpl) - getProtocolProvider().getOperationSet( - OperationSetBasicAutoAnswer.class); - - if (autoAnswerOpSet != null) - autoAnswerOpSet.autoAnswer(this, directions); - - return callPeer; - } - - /** - * Updates the state of the local DTLS-SRTP endpoint (i.e. the local - * DtlsControl instance) from the state of the remote DTLS-SRTP - * endpoint represented by a specific ColibriConferenceIQ.Channel. - * - * @param peer the CallPeer associated with the method invocation - * @param channel the ColibriConferenceIQ.Channel which represents - * the state of the remote DTLS-SRTP endpoint - * @param mediaType the MediaType of the media to be transmitted - * over the DTLS-SRTP session - * @param true if DTLS-SRTP has been selected by the local peer as - * the secure transport; otherwise, false - */ - private boolean addDtlsAdvertisedEncryptions( - CallPeerJabberImpl peer, - ColibriConferenceIQ.Channel channel, - MediaType mediaType) - { - CallPeerMediaHandlerJabberImpl peerMediaHandler - = peer.getMediaHandler(); - DtlsControl dtlsControl - = (DtlsControl) - peerMediaHandler.getSrtpControls().get( - mediaType, - SrtpControlType.DTLS_SRTP); - - if (dtlsControl != null) - { - /* - * Jitsi Videobridge is a server-side endpoint and thus is supposed - * to have a public IP so it makes sense to start the DTLS-SRTP - * endpoint represented by this Call as a client. - */ - dtlsControl.setDtlsProtocol(DtlsControl.DTLS_CLIENT_PROTOCOL); - } - - IceUdpTransportPacketExtension remoteTransport = channel.getTransport(); - - return - peerMediaHandler.addDtlsAdvertisedEncryptions( - true, - remoteTransport, - mediaType); - } - - /** - * Updates the state of the remote DTLS-SRTP endpoint represented by a - * specific ColibriConferenceIQ.Channel from the state of the local - * DTLS-SRTP endpoint. The specified channel is to be used by the - * conference focus for the purposes of transmitting media between a remote - * peer and the Jitsi Videobridge server. - * - * @param mediaType the MediaType of the media to be transmitted - * over the DTLS-SRTP session - * @param localContent the ContentPacketExtension of the local peer - * in the negotiation between the local and the remote peers. If - * remoteContent is null, represents an offer from the - * local peer to the remote peer; otherwise, represents an answer from the - * local peer to an offer from the remote peer. - * @param remoteContent the ContentPacketExtension, if any, of the - * remote peer in the negotiation between the local and the remote peers. If - * null, localContent represents an offer from the local - * peer to the remote peer; otherwise, localContent represents an - * answer from the local peer to an offer from the remote peer - * @param peer the CallPeer which represents the remote peer and - * which is associated with the specified channel - * @param channel the ColibriConferenceIQ.Channel which represents - * the state of the remote DTLS-SRTP endpoint. - */ - private void setDtlsEncryptionOnChannel( - MediaType mediaType, - ContentPacketExtension localContent, - ContentPacketExtension remoteContent, - CallPeerJabberImpl peer, - ColibriConferenceIQ.Channel channel) - { - AccountID accountID = getProtocolProvider().getAccountID(); - - if (accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.DEFAULT_ENCRYPTION, - true) - && accountID.isEncryptionProtocolEnabled( - DtlsControl.PROTO_NAME) - && (remoteContent != null)) - { - IceUdpTransportPacketExtension remoteTransport - = remoteContent.getFirstChildOfType( - IceUdpTransportPacketExtension.class); - - if (remoteTransport != null) - { - List remoteFingerprints - = remoteTransport.getChildExtensionsOfType( - DtlsFingerprintPacketExtension.class); - - if (!remoteFingerprints.isEmpty()) - { - IceUdpTransportPacketExtension localTransport - = ensureTransportOnChannel(channel, peer); - - if (localTransport != null) - { - List localFingerprints - = localTransport.getChildExtensionsOfType( - DtlsFingerprintPacketExtension.class); - - if (localFingerprints.isEmpty()) - { - for (DtlsFingerprintPacketExtension remoteFingerprint - : remoteFingerprints) - { - DtlsFingerprintPacketExtension localFingerprint - = new DtlsFingerprintPacketExtension(); - - localFingerprint.setFingerprint( - remoteFingerprint.getFingerprint()); - localFingerprint.setHash( - remoteFingerprint.getHash()); - localTransport.addChildExtension( - localFingerprint); - } - } - } - } - } - } - } - - /** - * Updates the state of the remote DTLS-SRTP endpoint represented by a - * specific ColibriConferenceIQ.Channel from the state of the local - * DTLS-SRTP endpoint (i.e. the local DtlsControl instance). The - * specified channel is to be used by the conference focus for the - * purposes of transmitting media between the local peer and the Jitsi - * Videobridge server. - * - * @param jitsiVideobridge the address/JID of the Jitsi Videobridge - * @param peer the CallPeer associated with the method invocation - * @param mediaType the MediaType of the media to be transmitted - * over the DTLS-SRTP session - * @param channel the ColibriConferenceIQ.Channel which represents - * the state of the remote DTLS-SRTP endpoint. - */ - private void setDtlsEncryptionOnChannel( - String jitsiVideobridge, - CallPeerJabberImpl peer, - MediaType mediaType, - ColibriConferenceIQ.Channel channel) - { - ProtocolProviderServiceJabberImpl protocolProvider - = getProtocolProvider(); - AccountID accountID = protocolProvider.getAccountID(); - - if (accountID.getAccountPropertyBoolean( - ProtocolProviderFactory.DEFAULT_ENCRYPTION, - true) - && accountID.isEncryptionProtocolEnabled( - DtlsControl.PROTO_NAME) - && protocolProvider.isFeatureSupported( - jitsiVideobridge, - ProtocolProviderServiceJabberImpl - .URN_XMPP_JINGLE_DTLS_SRTP)) - { - CallPeerMediaHandlerJabberImpl mediaHandler - = peer.getMediaHandler(); - DtlsControl dtlsControl - = (DtlsControl) - mediaHandler.getSrtpControls().getOrCreate( - mediaType, - SrtpControlType.DTLS_SRTP); - - if (dtlsControl != null) - { - IceUdpTransportPacketExtension transport - = ensureTransportOnChannel(channel, peer); - - if (transport != null) - setDtlsEncryptionOnTransport(dtlsControl, transport); - } - } - } - - /** - * Sets the properties (i.e. fingerprint and hash function) of a specific - * DtlsControl on the specific - * IceUdpTransportPacketExtension. - * - * @param dtlsControl the DtlsControl the properties of which are - * to be set on the specified localTransport - * @param localTransport the IceUdpTransportPacketExtension on - * which the properties of the specified dtlsControl are to be set - */ - static void setDtlsEncryptionOnTransport( - DtlsControl dtlsControl, - IceUdpTransportPacketExtension localTransport) - { - String fingerprint = dtlsControl.getLocalFingerprint(); - String hash = dtlsControl.getLocalFingerprintHashFunction(); - - DtlsFingerprintPacketExtension fingerprintPE - = localTransport.getFirstChildOfType( - DtlsFingerprintPacketExtension.class); - - if (fingerprintPE == null) - { - fingerprintPE = new DtlsFingerprintPacketExtension(); - localTransport.addChildExtension(fingerprintPE); - } - fingerprintPE.setFingerprint(fingerprint); - fingerprintPE.setHash(hash); - } - - private void setTransportOnChannel( - CallPeerJabberImpl peer, - String media, - ColibriConferenceIQ.Channel channel) - throws OperationFailedException - { - PacketExtension transport - = peer.getMediaHandler().getTransportManager().createTransport( - media); - - if (transport instanceof IceUdpTransportPacketExtension) - channel.setTransport((IceUdpTransportPacketExtension) transport); - } - - private void setTransportOnChannel( - String media, - ContentPacketExtension localContent, - ContentPacketExtension remoteContent, - CallPeerJabberImpl peer, - ColibriConferenceIQ.Channel channel) - throws OperationFailedException - { - if (remoteContent != null) - { - IceUdpTransportPacketExtension transport - = remoteContent.getFirstChildOfType( - IceUdpTransportPacketExtension.class); - - channel.setTransport( - TransportManagerJabberImpl.cloneTransportAndCandidates( - transport)); - } - } - - /** - * Makes an attempt to ensure that a specific - * ColibriConferenceIQ.Channel has a non-null - * transport set. If the specified channel does not have - * a transport, the method invokes the TransportManager of - * the specified CallPeerJabberImpl to initialize a new - * PacketExtension. - * - * @param channel the ColibriConferenceIQ.Channel to ensure the - * transport on - * @param peer the CallPeerJabberImpl which is associated with the - * specified channel and which specifies the - * TransportManager to be described in the specified - * channel - * @return the transport of the specified channel - */ - private IceUdpTransportPacketExtension ensureTransportOnChannel( - ColibriConferenceIQ.Channel channel, - CallPeerJabberImpl peer) - { - IceUdpTransportPacketExtension transport - = channel.getTransport(); - - if (transport == null) - { - PacketExtension pe - = peer - .getMediaHandler() - .getTransportManager() - .createTransportPacketExtension(); - - if (pe instanceof IceUdpTransportPacketExtension) - { - transport = (IceUdpTransportPacketExtension) pe; - channel.setTransport(transport); - } - } - return transport; - } -} +/* + * 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.lang.ref.*; +import java.util.*; + +import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; +import net.java.sip.communicator.impl.protocol.jabber.jinglesdp.*; +import net.java.sip.communicator.service.protocol.*; +import net.java.sip.communicator.service.protocol.event.*; +import net.java.sip.communicator.service.protocol.media.*; +import net.java.sip.communicator.util.*; + +import org.jitsi.service.neomedia.*; +import org.jivesoftware.smack.*; +import org.jivesoftware.smack.filter.*; +import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smackx.packet.*; + +/** + * A Jabber implementation of the Call abstract class encapsulating + * Jabber jingle sessions. + * + * @author Emil Ivov + * @author Lyubomir Marinov + * @author Boris Grozev + */ +public class CallJabberImpl + extends AbstractCallJabberGTalkImpl +{ + /** + * The Logger used by the CallJabberImpl class and its + * instances for logging output. + */ + private static final Logger logger = Logger.getLogger(CallJabberImpl.class); + + /** + * The Jitsi Videobridge conference which the local peer represented by this + * instance is a focus of. + */ + private ColibriConferenceIQ colibri; + + /** + * The shared CallPeerMediaHandler state which is to be used by the + * CallPeers of this Call which use {@link #colibri}. + */ + private MediaHandler colibriMediaHandler; + + /** + * Contains one ColibriStreamConnector for each MediaType + */ + private final List> + colibriStreamConnectors; + + /** + * The entity ID of the Jitsi Videobridge to be utilized by this + * Call for the purposes of establishing a server-assisted + * telephony conference. + */ + private String jitsiVideobridge; + + /** + * Initializes a new CallJabberImpl instance. + * + * @param parentOpSet the {@link OperationSetBasicTelephonyJabberImpl} + * instance in the context of which this call has been created. + */ + protected CallJabberImpl( + OperationSetBasicTelephonyJabberImpl parentOpSet) + { + super(parentOpSet); + + int mediaTypeValueCount = MediaType.values().length; + + colibriStreamConnectors + = new ArrayList>( + mediaTypeValueCount); + for (int i = 0; i < mediaTypeValueCount; i++) + colibriStreamConnectors.add(null); + + //let's add ourselves to the calls repo. we are doing it ourselves just + //to make sure that no one ever forgets. + parentOpSet.getActiveCallsRepository().addCall(this); + } + + /** + * Closes a specific ColibriStreamConnector which is associated with + * a MediaStream of a specific MediaType upon request from + * a specific CallPeer. + * + * @param peer the CallPeer which requests the closing of the + * specified colibriStreamConnector + * @param mediaType the MediaType of the MediaStream with + * which the specified colibriStreamConnector is associated + * @param colibriStreamConnector the ColibriStreamConnector to close on + * behalf of the specified peer + */ + public void closeColibriStreamConnector( + CallPeerJabberImpl peer, + MediaType mediaType, + ColibriStreamConnector colibriStreamConnector) + { + colibriStreamConnector.close(); + synchronized (colibriStreamConnectors) + { + int index = mediaType.ordinal(); + WeakReference weakReference + = colibriStreamConnectors.get(index); + if (weakReference != null && colibriStreamConnector + .equals(weakReference.get())) + { + colibriStreamConnectors.set(index, null); + } + } + } + + /** + * {@inheritDoc} + * + * Sends a content message to each of the CallPeers + * associated with this CallJabberImpl in order to include/exclude + * the "isfocus" attribute. + */ + @Override + protected void conferenceFocusChanged(boolean oldValue, boolean newValue) + { + try + { + Iterator peers = getCallPeers(); + + while (peers.hasNext()) + { + CallPeerJabberImpl callPeer = peers.next(); + + if (callPeer.getState() == CallPeerState.CONNECTED) + callPeer.sendCoinSessionInfo(); + } + } + finally + { + super.conferenceFocusChanged(oldValue, newValue); + } + } + + /** + * Allocates colibri (conference) channels for a specific MediaType + * to be used by a specific CallPeer. + * + * @param peer the CallPeer which is to use the allocated colibri + * (conference) channels + * @param contentMap the local and remote ContentPacketExtensions + * which specify the MediaTypes for which colibri (conference) + * channels are to be allocated + * @return a ColibriConferenceIQ which describes the allocated + * colibri (conference) channels for the specified mediaTypes which + * are to be used by the specified peer; otherwise, null + */ + public ColibriConferenceIQ createColibriChannels( + CallPeerJabberImpl peer, + Map contentMap) + throws OperationFailedException + { + if (!getConference().isJitsiVideobridge()) + return null; + + /* + * For a colibri conference to work properly, all CallPeers in the + * conference must share one and the same CallPeerMediaHandler state + * i.e. they must use a single set of MediaStreams as if there was a + * single CallPeerMediaHandler. + */ + CallPeerMediaHandlerJabberImpl peerMediaHandler + = peer.getMediaHandler(); + + if (peerMediaHandler.getMediaHandler() != colibriMediaHandler) + { + for (MediaType mediaType : MediaType.values()) + { + if (peerMediaHandler.getStream(mediaType) != null) + return null; + } + } + + ProtocolProviderServiceJabberImpl protocolProvider + = getProtocolProvider(); + String jitsiVideobridge + = (colibri == null) ? getJitsiVideobridge() : colibri.getFrom(); + + if ((jitsiVideobridge == null) || (jitsiVideobridge.length() == 0)) + { + logger.error( + "Failed to allocate colibri channels: no videobridge" + + " found."); + return null; + } + + /* + * The specified CallPeer will participate in the colibri conference + * organized by this Call so it must use the shared CallPeerMediaHandler + * state of all CallPeers in the same colibri conference. + */ + if (colibriMediaHandler == null) + colibriMediaHandler = new MediaHandler(); + peerMediaHandler.setMediaHandler(colibriMediaHandler); + + ColibriConferenceIQ conferenceRequest = new ColibriConferenceIQ(); + + if (colibri != null) + conferenceRequest.setID(colibri.getID()); + + for (Map.Entry e + : contentMap.entrySet()) + { + ContentPacketExtension localContent = e.getKey(); + ContentPacketExtension remoteContent = e.getValue(); + ContentPacketExtension cpe + = (remoteContent == null) ? localContent : remoteContent; + RtpDescriptionPacketExtension rdpe + = cpe.getFirstChildOfType( + RtpDescriptionPacketExtension.class); + String media = rdpe.getMedia(); + MediaType mediaType = MediaType.parseString(media); + String contentName = mediaType.toString(); + ColibriConferenceIQ.Content contentRequest + = new ColibriConferenceIQ.Content(contentName); + + conferenceRequest.addContent(contentRequest); + + boolean requestLocalChannel = true; + + if (colibri != null) + { + ColibriConferenceIQ.Content content + = colibri.getContent(contentName); + + if ((content != null) && (content.getChannelCount() > 0)) + requestLocalChannel = false; + } + + boolean peerIsInitiator = peer.isInitiator(); + + if (requestLocalChannel) + { + ColibriConferenceIQ.Channel localChannelRequest + = new ColibriConferenceIQ.Channel(); + + localChannelRequest.setInitiator(peerIsInitiator); + + for (PayloadTypePacketExtension ptpe : rdpe.getPayloadTypes()) + localChannelRequest.addPayloadType(ptpe); + setTransportOnChannel(peer, media, localChannelRequest); + // DTLS-SRTP + setDtlsEncryptionOnChannel( + jitsiVideobridge, + peer, + mediaType, + localChannelRequest); + /* + * Since Jitsi Videobridge supports multiple Jingle transports, + * it is a good idea to indicate which one is expected on a + * channel. + */ + ensureTransportOnChannel(localChannelRequest, peer); + contentRequest.addChannel(localChannelRequest); + } + + ColibriConferenceIQ.Channel remoteChannelRequest + = new ColibriConferenceIQ.Channel(); + + remoteChannelRequest.setInitiator(!peerIsInitiator); + + for (PayloadTypePacketExtension ptpe : rdpe.getPayloadTypes()) + remoteChannelRequest.addPayloadType(ptpe); + setTransportOnChannel( + media, + localContent, + remoteContent, + peer, + remoteChannelRequest); + // DTLS-SRTP + setDtlsEncryptionOnChannel( + mediaType, + localContent, + remoteContent, + peer, + remoteChannelRequest); + /* + * Since Jitsi Videobridge supports multiple Jingle transports, it + * is a good idea to indicate which one is expected on a channel. + */ + ensureTransportOnChannel(remoteChannelRequest, peer); + contentRequest.addChannel(remoteChannelRequest); + } + + XMPPConnection connection = protocolProvider.getConnection(); + PacketCollector packetCollector + = connection.createPacketCollector( + new PacketIDFilter(conferenceRequest.getPacketID())); + + conferenceRequest.setTo(jitsiVideobridge); + conferenceRequest.setType(IQ.Type.GET); + connection.sendPacket(conferenceRequest); + + Packet response + = packetCollector.nextResult( + SmackConfiguration.getPacketReplyTimeout()); + + packetCollector.cancel(); + + if (response == null) + { + logger.error( + "Failed to allocate colibri channels: response is null." + + " Maybe the response timed out."); + return null; + } + else if (response.getError() != null) + { + logger.error( + "Failed to allocate colibri channels: " + + response.getError()); + return null; + } + else if (!(response instanceof ColibriConferenceIQ)) + { + logger.error( + "Failed to allocate colibri channels: response is not a" + + " colibri conference"); + return null; + } + + ColibriConferenceIQ conferenceResponse = (ColibriConferenceIQ) response; + String conferenceResponseID = conferenceResponse.getID(); + + /* + * Update the complete ColibriConferenceIQ representation maintained by + * this instance with the information given by the (current) response. + */ + { + if (colibri == null) + { + colibri = new ColibriConferenceIQ(); + /* + * XXX We must remember the JID of the Jitsi Videobridge because + * (1) we do not want to re-discover it in every method + * invocation on this Call instance and (2) we want to use one + * and the same for all CallPeers within this Call instance. + */ + colibri.setFrom(conferenceResponse.getFrom()); + } + + String colibriID = colibri.getID(); + + if (colibriID == null) + colibri.setID(conferenceResponseID); + else if (!colibriID.equals(conferenceResponseID)) + throw new IllegalStateException("conference.id"); + + for (ColibriConferenceIQ.Content contentResponse + : conferenceResponse.getContents()) + { + String contentName = contentResponse.getName(); + ColibriConferenceIQ.Content content + = colibri.getOrCreateContent(contentName); + + for (ColibriConferenceIQ.Channel channelResponse + : contentResponse.getChannels()) + { + int channelIndex = content.getChannelCount(); + + content.addChannel(channelResponse); + if (channelIndex == 0) + { + TransportManagerJabberImpl transportManager + = peerMediaHandler.getTransportManager(); + + transportManager + .isEstablishingConnectivityWithJitsiVideobridge + = true; + transportManager + .startConnectivityEstablishmentWithJitsiVideobridge + = true; + + MediaType mediaType + = MediaType.parseString(contentName); + + // DTLS-SRTP + addDtlsAdvertisedEncryptions( + peer, + channelResponse, + mediaType); + } + } + } + } + + /* + * Formulate the result to be returned to the caller which is a subset + * of the whole conference information kept by this CallJabberImpl and + * includes the remote channels explicitly requested by the method + * caller and their respective local channels. + */ + ColibriConferenceIQ conferenceResult = new ColibriConferenceIQ(); + + conferenceResult.setFrom(colibri.getFrom()); + conferenceResult.setID(conferenceResponseID); + + for (Map.Entry e + : contentMap.entrySet()) + { + ContentPacketExtension localContent = e.getKey(); + ContentPacketExtension remoteContent = e.getValue(); + ContentPacketExtension cpe + = (remoteContent == null) ? localContent : remoteContent; + RtpDescriptionPacketExtension rdpe + = cpe.getFirstChildOfType( + RtpDescriptionPacketExtension.class); + MediaType mediaType = MediaType.parseString(rdpe.getMedia()); + ColibriConferenceIQ.Content contentResponse + = conferenceResponse.getContent(mediaType.toString()); + + if (contentResponse != null) + { + String contentName = contentResponse.getName(); + ColibriConferenceIQ.Content contentResult + = new ColibriConferenceIQ.Content(contentName); + + conferenceResult.addContent(contentResult); + + /* + * The local channel may have been allocated in a previous + * method call as part of the allocation of the first remote + * channel in the respective content. Anyway, the current method + * caller still needs to know about it. + */ + ColibriConferenceIQ.Content content + = colibri.getContent(contentName); + ColibriConferenceIQ.Channel localChannel = null; + + if ((content != null) && (content.getChannelCount() > 0)) + { + localChannel = content.getChannel(0); + contentResult.addChannel(localChannel); + } + + String localChannelID + = (localChannel == null) ? null : localChannel.getID(); + + for (ColibriConferenceIQ.Channel channelResponse + : contentResponse.getChannels()) + { + if ((localChannelID == null) + || !localChannelID.equals(channelResponse.getID())) + contentResult.addChannel(channelResponse); + } + } + } + + return conferenceResult; + } + + /** + * Initializes a ColibriStreamConnector on behalf of a specific + * CallPeer to be used in association with a specific + * ColibriConferenceIQ.Channel of a specific MediaType. + * + * @param peer the CallPeer which requests the initialization of a + * ColibriStreamConnector + * @param mediaType the MediaType of the stream which is to use the + * initialized ColibriStreamConnector for RTP and RTCP traffic + * @param channel the ColibriConferenceIQ.Channel to which RTP and + * RTCP traffic is to be sent and from which such traffic is to be received + * via the initialized ColibriStreamConnector + * @param factory a StreamConnectorFactory implementation which is + * to allocate the sockets to be used for RTP and RTCP traffic + * @return a ColibriStreamConnector to be used for RTP and RTCP + * traffic associated with the specified channel + */ + public ColibriStreamConnector createColibriStreamConnector( + CallPeerJabberImpl peer, + MediaType mediaType, + ColibriConferenceIQ.Channel channel, + StreamConnectorFactory factory) + { + String channelID = channel.getID(); + + if (channelID == null) + throw new IllegalArgumentException("channel"); + + if (colibri == null) + throw new IllegalStateException("colibri"); + + ColibriConferenceIQ.Content content + = colibri.getContent(mediaType.toString()); + + if (content == null) + throw new IllegalArgumentException("mediaType"); + if ((content.getChannelCount() < 1) + || !channelID.equals((channel = content.getChannel(0)).getID())) + throw new IllegalArgumentException("channel"); + + ColibriStreamConnector colibriStreamConnector; + + synchronized (colibriStreamConnectors) + { + int index = mediaType.ordinal(); + WeakReference weakReference + = colibriStreamConnectors.get(index); + + colibriStreamConnector + = (weakReference == null) ? null : weakReference.get(); + if (colibriStreamConnector == null) + { + StreamConnector streamConnector + = factory.createStreamConnector(); + + if (streamConnector != null) + { + colibriStreamConnector + = new ColibriStreamConnector(streamConnector); + colibriStreamConnectors.set( + index, + new WeakReference( + colibriStreamConnector)); + } + } + } + + return colibriStreamConnector; + } + + /** + * Expires specific (colibri) conference channels used by a specific + * CallPeer. + * + * @param peer the CallPeer which uses the specified (colibri) + * conference channels to be expired + * @param conference a ColibriConferenceIQ which specifies the + * (colibri) conference channels to be expired + */ + public void expireColibriChannels( + CallPeerJabberImpl peer, + ColibriConferenceIQ conference) + { + // Formulate the ColibriConferenceIQ request which is to be sent. + if (colibri != null) + { + String conferenceID = colibri.getID(); + + if (conferenceID.equals(conference.getID())) + { + ColibriConferenceIQ conferenceRequest + = new ColibriConferenceIQ(); + + conferenceRequest.setID(conferenceID); + + for (ColibriConferenceIQ.Content content + : conference.getContents()) + { + ColibriConferenceIQ.Content colibriContent + = colibri.getContent(content.getName()); + + if (colibriContent != null) + { + ColibriConferenceIQ.Content contentRequest + = conferenceRequest.getOrCreateContent( + colibriContent.getName()); + + for (ColibriConferenceIQ.Channel channel + : content.getChannels()) + { + ColibriConferenceIQ.Channel colibriChannel + = colibriContent.getChannel(channel.getID()); + + if (colibriChannel != null) + { + ColibriConferenceIQ.Channel channelRequest + = new ColibriConferenceIQ.Channel(); + + channelRequest.setExpire(0); + channelRequest.setID(colibriChannel.getID()); + contentRequest.addChannel(channelRequest); + } + } + } + } + + /* + * Remove the channels which are to be expired from the internal + * state of the conference managed by this CallJabberImpl. + */ + for (ColibriConferenceIQ.Content contentRequest + : conferenceRequest.getContents()) + { + ColibriConferenceIQ.Content colibriContent + = colibri.getContent(contentRequest.getName()); + + for (ColibriConferenceIQ.Channel channelRequest + : contentRequest.getChannels()) + { + ColibriConferenceIQ.Channel colibriChannel + = colibriContent.getChannel(channelRequest.getID()); + + colibriContent.removeChannel(colibriChannel); + + /* + * If the last remote channel is to be expired, expire + * the local channel as well. + */ + if (colibriContent.getChannelCount() == 1) + { + colibriChannel = colibriContent.getChannel(0); + + channelRequest = new ColibriConferenceIQ.Channel(); + channelRequest.setExpire(0); + channelRequest.setID(colibriChannel.getID()); + contentRequest.addChannel(channelRequest); + + colibriContent.removeChannel(colibriChannel); + + break; + } + } + } + + /* + * At long last, send the ColibriConferenceIQ request to expire + * the channels. + */ + conferenceRequest.setTo(colibri.getFrom()); + conferenceRequest.setType(IQ.Type.SET); + getProtocolProvider().getConnection().sendPacket( + conferenceRequest); + } + } + } + + /** + * Sends a ColibriConferenceIQ to the videobridge used by this + * CallJabberImpl, in order to request the the direction of + * the channel with ID channelID be set to + * direction + * @param channelID the ID of the channel for which to set the + * direction. + * @param mediaType the MediaType of the channel (we can deduce this + * by searching the ColibriConferenceIQ, but it's more convenient + * to have it) + * @param direction the MediaDirection to set. + */ + public void setChannelDirection(String channelID, + MediaType mediaType, + MediaDirection direction) + { + if ((colibri != null) && (channelID != null)) + { + ColibriConferenceIQ.Content content + = colibri.getContent(mediaType.toString()); + + if (content != null) + { + ColibriConferenceIQ.Channel channel + = content.getChannel(channelID); + + /* + * Note that we send requests even when the local Channel's + * direction and the direction we are setting are the same. We + * can easily avoid this, but we risk not sending necessary + * packets if local Channel and the actual channel on the + * videobridge are out of sync. + */ + if (channel != null) + { + ColibriConferenceIQ.Channel requestChannel + = new ColibriConferenceIQ.Channel(); + + requestChannel.setID(channelID); + requestChannel.setDirection(direction); + + ColibriConferenceIQ.Content requestContent + = new ColibriConferenceIQ.Content(); + + requestContent.setName(mediaType.toString()); + requestContent.addChannel(requestChannel); + + ColibriConferenceIQ conferenceRequest + = new ColibriConferenceIQ(); + + conferenceRequest.setID(colibri.getID()); + conferenceRequest.setTo(colibri.getFrom()); + conferenceRequest.setType(IQ.Type.SET); + conferenceRequest.addContent(requestContent); + + getProtocolProvider().getConnection().sendPacket( + conferenceRequest); + } + } + } + } + + /** + * Creates a CallPeerJabberImpl from calleeJID and sends + * them session-initiate IQ request. + * + * @param calleeJID the party that we would like to invite to this call. + * @param discoverInfo any discovery information that we have for the jid + * we are trying to reach and that we are passing in order to avoid having + * to ask for it again. + * @param sessionInitiateExtensions a collection of additional and optional + * PacketExtensions to be added to the session-initiate + * {@link JingleIQ} which is to init this CallJabberImpl + * @param supportedTransports the XML namespaces of the jingle transports + * to use. + * + * @return the newly created CallPeerJabberImpl corresponding to + * calleeJID. All following state change events will be + * delivered through this call peer. + * + * @throws OperationFailedException with the corresponding code if we fail + * to create the call. + */ + public CallPeerJabberImpl initiateSession( + String calleeJID, + DiscoverInfo discoverInfo, + Iterable sessionInitiateExtensions, + Collection supportedTransports) + throws OperationFailedException + { + // create the session-initiate IQ + CallPeerJabberImpl callPeer = new CallPeerJabberImpl(calleeJID, this); + + callPeer.setDiscoveryInfo(discoverInfo); + + addCallPeer(callPeer); + + callPeer.setState(CallPeerState.INITIATING_CALL); + + // If this was the first peer we added in this call, then the call is + // new and we need to notify everyone of its creation. + if (getCallPeerCount() == 1) + parentOpSet.fireCallEvent(CallEvent.CALL_INITIATED, this); + + CallPeerMediaHandlerJabberImpl mediaHandler + = callPeer.getMediaHandler(); + + //set the supported transports before the transport manager is created + mediaHandler.setSupportedTransports(supportedTransports); + + /* enable video if it is a video call */ + mediaHandler.setLocalVideoTransmissionEnabled(localVideoAllowed); + /* enable remote-control if it is a desktop sharing session */ + mediaHandler.setLocalInputEvtAware(getLocalInputEvtAware()); + + /* + * Set call state to connecting so that the user interface would start + * playing the tones. We do that here because we may be harvesting + * STUN/TURN addresses in initiateSession() which would take a while. + */ + callPeer.setState(CallPeerState.CONNECTING); + + // if initializing session fails, set peer to failed + boolean sessionInitiated = false; + + try + { + callPeer.initiateSession(sessionInitiateExtensions); + sessionInitiated = true; + } + finally + { + // if initialization throws an exception + if (!sessionInitiated) + callPeer.setState(CallPeerState.FAILED); + } + return callPeer; + } + + /** + * Updates the Jingle sessions for the CallPeers of this + * Call, to reflect the current state of the the video contents of + * this Call. Sends a content-modify, content-add + * or content-remove message to each of the current + * CallPeers. + * + * @throws OperationFailedException if a problem occurred during message + * generation or there was a network problem + */ + @Override + public void modifyVideoContent() + throws OperationFailedException + { + if (logger.isDebugEnabled()) + logger.debug("Updating video content for " + this); + + boolean change = false; + for (CallPeerJabberImpl peer : getCallPeerList()) + change |= peer.sendModifyVideoContent(); + + if (change) + fireCallChangeEvent( + CallChangeEvent.CALL_PARTICIPANTS_CHANGE, null, null); + } + + /** + * Notifies this instance that a specific ColibriConferenceIQ has + * been received. + * + * @param conferenceIQ the ColibriConferenceIQ which has been + * received + * @return true if the specified conferenceIQ was + * processed by this instance and no further processing is to be performed + * by other possible processors of ColibriConferenceIQs; otherwise, + * false. Because a ColibriConferenceIQ request sent from + * the Jitsi Videobridge server to the application as its client concerns a + * specific CallJabberImpl implementation, no further processing by + * other CallJabberImpl instances is necessary once the + * ColibriConferenceIQ is processed by the associated + * CallJabberImpl instance. + */ + boolean processColibriConferenceIQ(ColibriConferenceIQ conferenceIQ) + { + if (colibri == null) + { + /* + * This instance has not set up any conference using the Jitsi + * Videobridge server-side technology yet so it cannot be bothered + * with related requests. + */ + return false; + } + else if (conferenceIQ.getID().equals(colibri.getID())) + { + /* + * Remove the local Channels (from the specified conferenceIQ) i.e. + * the Channels on which the local peer/user is sending to the Jitsi + * Videobridge server because they concern this Call only and not + * its CallPeers. + */ + for (MediaType mediaType : MediaType.values()) + { + String contentName = mediaType.toString(); + ColibriConferenceIQ.Content content + = conferenceIQ.getContent(contentName); + + if (content != null) + { + ColibriConferenceIQ.Content thisContent + = colibri.getContent(contentName); + + if ((thisContent != null) + && (thisContent.getChannelCount() > 0)) + { + ColibriConferenceIQ.Channel thisChannel + = thisContent.getChannel(0); + ColibriConferenceIQ.Channel channel + = content.getChannel(thisChannel.getID()); + + if (channel != null) + content.removeChannel(channel); + } + } + } + + for (CallPeerJabberImpl callPeer : getCallPeerList()) + callPeer.processColibriConferenceIQ(conferenceIQ); + + /* + * We have removed the local Channels from the specified + * conferenceIQ. Consequently, it is no longer the same and fit for + * processing by other CallJabberImpl instances. + */ + return true; + } + else + { + /* + * This instance has set up a conference using the Jitsi Videobridge + * server-side technology but it is not the one referred to by the + * specified conferenceIQ i.e. the specified conferenceIQ does not + * concern this instance. + */ + return false; + } + } + + /** + * Creates a new call peer and sends a RINGING response. + * + * @param jingleIQ the {@link JingleIQ} that created the session. + * + * @return the newly created {@link CallPeerJabberImpl} (the one that sent + * the INVITE). + */ + public CallPeerJabberImpl processSessionInitiate(JingleIQ jingleIQ) + { + String remoteParty = jingleIQ.getInitiator(); + boolean autoAnswer = false; + CallPeerJabberImpl attendant = null; + OperationSetBasicTelephonyJabberImpl basicTelephony = null; + + //according to the Jingle spec initiator may be null. + if (remoteParty == null) + remoteParty = jingleIQ.getFrom(); + + CallPeerJabberImpl callPeer + = new CallPeerJabberImpl(remoteParty, this, jingleIQ); + + addCallPeer(callPeer); + + /* + * We've already sent ack to the specified session-initiate so if it has + * been sent as part of an attended transfer, we have to hang up on the + * attendant. + */ + try + { + TransferPacketExtension transfer + = (TransferPacketExtension) + jingleIQ.getExtension( + TransferPacketExtension.ELEMENT_NAME, + TransferPacketExtension.NAMESPACE); + + if (transfer != null) + { + String sid = transfer.getSID(); + + if (sid != null) + { + ProtocolProviderServiceJabberImpl protocolProvider + = getProtocolProvider(); + basicTelephony + = (OperationSetBasicTelephonyJabberImpl) + protocolProvider.getOperationSet( + OperationSetBasicTelephony.class); + CallJabberImpl attendantCall + = basicTelephony + .getActiveCallsRepository() + .findSID(sid); + + if (attendantCall != null) + { + attendant = attendantCall.getPeer(sid); + if ((attendant != null) + && basicTelephony + .getFullCalleeURI(attendant.getAddress()) + .equals(transfer.getFrom()) + && protocolProvider.getOurJID().equals( + transfer.getTo())) + { + //basicTelephony.hangupCallPeer(attendant); + autoAnswer = true; + } + } + } + } + } + catch (Throwable t) + { + logger.error( + "Failed to hang up on attendant" + + " as part of session transfer", + t); + + if (t instanceof ThreadDeath) + throw (ThreadDeath) t; + } + + CoinPacketExtension coin + = (CoinPacketExtension) + jingleIQ.getExtension( + CoinPacketExtension.ELEMENT_NAME, + CoinPacketExtension.NAMESPACE); + + if (coin != null) + { + boolean b + = Boolean.parseBoolean( + (String) + coin.getAttribute( + CoinPacketExtension.ISFOCUS_ATTR_NAME)); + + callPeer.setConferenceFocus(b); + } + + //before notifying about this call, make sure that it looks alright + callPeer.processSessionInitiate(jingleIQ); + + // if paranoia is set, to accept the call we need to know that + // the other party has support for media encryption + if (getProtocolProvider().getAccountID().getAccountPropertyBoolean( + ProtocolProviderFactory.MODE_PARANOIA, false) + && callPeer.getMediaHandler().getAdvertisedEncryptionMethods() + .length + == 0) + { + //send an error response; + String reasonText + = JabberActivator.getResources().getI18NString( + "service.gui.security.encryption.required"); + JingleIQ errResp + = JinglePacketFactory.createSessionTerminate( + jingleIQ.getTo(), + jingleIQ.getFrom(), + jingleIQ.getSID(), + Reason.SECURITY_ERROR, + reasonText); + + callPeer.setState(CallPeerState.FAILED, reasonText); + getProtocolProvider().getConnection().sendPacket(errResp); + + return null; + } + + if (callPeer.getState() == CallPeerState.FAILED) + return null; + + callPeer.setState( CallPeerState.INCOMING_CALL ); + + // in case of attended transfer, auto answer the call + if (autoAnswer) + { + /* answer directly */ + try + { + callPeer.answer(); + } + catch(Exception e) + { + logger.info( + "Exception occurred while answer transferred call", + e); + callPeer = null; + } + + // hang up now + try + { + basicTelephony.hangupCallPeer(attendant); + } + catch(OperationFailedException e) + { + logger.error( + "Failed to hang up on attendant as part of session" + + " transfer", + e); + } + + return callPeer; + } + + /* see if offer contains audio and video so that we can propose + * option to the user (i.e. answer with video if it is a video call...) + */ + List offer + = callPeer.getSessionIQ().getContentList(); + Map directions + = new HashMap(); + + directions.put(MediaType.AUDIO, MediaDirection.INACTIVE); + directions.put(MediaType.VIDEO, MediaDirection.INACTIVE); + + for (ContentPacketExtension c : offer) + { + String contentName = c.getName(); + MediaDirection remoteDirection + = JingleUtils.getDirection(c, callPeer.isInitiator()); + + if (MediaType.AUDIO.toString().equals(contentName)) + directions.put(MediaType.AUDIO, remoteDirection); + else if (MediaType.VIDEO.toString().equals(contentName)) + directions.put(MediaType.VIDEO, remoteDirection); + } + + // If this was the first peer we added in this call, then the call is + // new and we need to notify everyone of its creation. + if (getCallPeerCount() == 1) + { + parentOpSet.fireCallEvent( + CallEvent.CALL_RECEIVED, + this, + directions); + } + + // Manages auto answer with "audio only", or "audio/video" answer. + OperationSetAutoAnswerJabberImpl autoAnswerOpSet + = (OperationSetAutoAnswerJabberImpl) + getProtocolProvider().getOperationSet( + OperationSetBasicAutoAnswer.class); + + if (autoAnswerOpSet != null) + autoAnswerOpSet.autoAnswer(this, directions); + + return callPeer; + } + + /** + * Updates the state of the local DTLS-SRTP endpoint (i.e. the local + * DtlsControl instance) from the state of the remote DTLS-SRTP + * endpoint represented by a specific ColibriConferenceIQ.Channel. + * + * @param peer the CallPeer associated with the method invocation + * @param channel the ColibriConferenceIQ.Channel which represents + * the state of the remote DTLS-SRTP endpoint + * @param mediaType the MediaType of the media to be transmitted + * over the DTLS-SRTP session + * @param true if DTLS-SRTP has been selected by the local peer as + * the secure transport; otherwise, false + */ + private boolean addDtlsAdvertisedEncryptions( + CallPeerJabberImpl peer, + ColibriConferenceIQ.Channel channel, + MediaType mediaType) + { + CallPeerMediaHandlerJabberImpl peerMediaHandler + = peer.getMediaHandler(); + DtlsControl dtlsControl + = (DtlsControl) + peerMediaHandler.getSrtpControls().get( + mediaType, + SrtpControlType.DTLS_SRTP); + + if (dtlsControl != null) + { + /* + * Jitsi Videobridge is a server-side endpoint and thus is supposed + * to have a public IP so it makes sense to start the DTLS-SRTP + * endpoint represented by this Call as a client. + */ + dtlsControl.setDtlsProtocol(DtlsControl.DTLS_CLIENT_PROTOCOL); + } + + IceUdpTransportPacketExtension remoteTransport = channel.getTransport(); + + return + peerMediaHandler.addDtlsAdvertisedEncryptions( + true, + remoteTransport, + mediaType); + } + + /** + * Updates the state of the remote DTLS-SRTP endpoint represented by a + * specific ColibriConferenceIQ.Channel from the state of the local + * DTLS-SRTP endpoint. The specified channel is to be used by the + * conference focus for the purposes of transmitting media between a remote + * peer and the Jitsi Videobridge server. + * + * @param mediaType the MediaType of the media to be transmitted + * over the DTLS-SRTP session + * @param localContent the ContentPacketExtension of the local peer + * in the negotiation between the local and the remote peers. If + * remoteContent is null, represents an offer from the + * local peer to the remote peer; otherwise, represents an answer from the + * local peer to an offer from the remote peer. + * @param remoteContent the ContentPacketExtension, if any, of the + * remote peer in the negotiation between the local and the remote peers. If + * null, localContent represents an offer from the local + * peer to the remote peer; otherwise, localContent represents an + * answer from the local peer to an offer from the remote peer + * @param peer the CallPeer which represents the remote peer and + * which is associated with the specified channel + * @param channel the ColibriConferenceIQ.Channel which represents + * the state of the remote DTLS-SRTP endpoint. + */ + private void setDtlsEncryptionOnChannel( + MediaType mediaType, + ContentPacketExtension localContent, + ContentPacketExtension remoteContent, + CallPeerJabberImpl peer, + ColibriConferenceIQ.Channel channel) + { + AccountID accountID = getProtocolProvider().getAccountID(); + + if (accountID.getAccountPropertyBoolean( + ProtocolProviderFactory.DEFAULT_ENCRYPTION, + true) + && accountID.isEncryptionProtocolEnabled( + DtlsControl.PROTO_NAME) + && (remoteContent != null)) + { + IceUdpTransportPacketExtension remoteTransport + = remoteContent.getFirstChildOfType( + IceUdpTransportPacketExtension.class); + + if (remoteTransport != null) + { + List remoteFingerprints + = remoteTransport.getChildExtensionsOfType( + DtlsFingerprintPacketExtension.class); + + if (!remoteFingerprints.isEmpty()) + { + IceUdpTransportPacketExtension localTransport + = ensureTransportOnChannel(channel, peer); + + if (localTransport != null) + { + List localFingerprints + = localTransport.getChildExtensionsOfType( + DtlsFingerprintPacketExtension.class); + + if (localFingerprints.isEmpty()) + { + for (DtlsFingerprintPacketExtension remoteFingerprint + : remoteFingerprints) + { + DtlsFingerprintPacketExtension localFingerprint + = new DtlsFingerprintPacketExtension(); + + localFingerprint.setFingerprint( + remoteFingerprint.getFingerprint()); + localFingerprint.setHash( + remoteFingerprint.getHash()); + localTransport.addChildExtension( + localFingerprint); + } + } + } + } + } + } + } + + /** + * Updates the state of the remote DTLS-SRTP endpoint represented by a + * specific ColibriConferenceIQ.Channel from the state of the local + * DTLS-SRTP endpoint (i.e. the local DtlsControl instance). The + * specified channel is to be used by the conference focus for the + * purposes of transmitting media between the local peer and the Jitsi + * Videobridge server. + * + * @param jitsiVideobridge the address/JID of the Jitsi Videobridge + * @param peer the CallPeer associated with the method invocation + * @param mediaType the MediaType of the media to be transmitted + * over the DTLS-SRTP session + * @param channel the ColibriConferenceIQ.Channel which represents + * the state of the remote DTLS-SRTP endpoint. + */ + private void setDtlsEncryptionOnChannel( + String jitsiVideobridge, + CallPeerJabberImpl peer, + MediaType mediaType, + ColibriConferenceIQ.Channel channel) + { + ProtocolProviderServiceJabberImpl protocolProvider + = getProtocolProvider(); + AccountID accountID = protocolProvider.getAccountID(); + + if (accountID.getAccountPropertyBoolean( + ProtocolProviderFactory.DEFAULT_ENCRYPTION, + true) + && accountID.isEncryptionProtocolEnabled( + DtlsControl.PROTO_NAME) + && protocolProvider.isFeatureSupported( + jitsiVideobridge, + ProtocolProviderServiceJabberImpl + .URN_XMPP_JINGLE_DTLS_SRTP)) + { + CallPeerMediaHandlerJabberImpl mediaHandler + = peer.getMediaHandler(); + DtlsControl dtlsControl + = (DtlsControl) + mediaHandler.getSrtpControls().getOrCreate( + mediaType, + SrtpControlType.DTLS_SRTP); + + if (dtlsControl != null) + { + IceUdpTransportPacketExtension transport + = ensureTransportOnChannel(channel, peer); + + if (transport != null) + setDtlsEncryptionOnTransport(dtlsControl, transport); + } + } + } + + /** + * Sets the properties (i.e. fingerprint and hash function) of a specific + * DtlsControl on the specific + * IceUdpTransportPacketExtension. + * + * @param dtlsControl the DtlsControl the properties of which are + * to be set on the specified localTransport + * @param localTransport the IceUdpTransportPacketExtension on + * which the properties of the specified dtlsControl are to be set + */ + static void setDtlsEncryptionOnTransport( + DtlsControl dtlsControl, + IceUdpTransportPacketExtension localTransport) + { + String fingerprint = dtlsControl.getLocalFingerprint(); + String hash = dtlsControl.getLocalFingerprintHashFunction(); + + DtlsFingerprintPacketExtension fingerprintPE + = localTransport.getFirstChildOfType( + DtlsFingerprintPacketExtension.class); + + if (fingerprintPE == null) + { + fingerprintPE = new DtlsFingerprintPacketExtension(); + localTransport.addChildExtension(fingerprintPE); + } + fingerprintPE.setFingerprint(fingerprint); + fingerprintPE.setHash(hash); + } + + private void setTransportOnChannel( + CallPeerJabberImpl peer, + String media, + ColibriConferenceIQ.Channel channel) + throws OperationFailedException + { + PacketExtension transport + = peer.getMediaHandler().getTransportManager().createTransport( + media); + + if (transport instanceof IceUdpTransportPacketExtension) + channel.setTransport((IceUdpTransportPacketExtension) transport); + } + + private void setTransportOnChannel( + String media, + ContentPacketExtension localContent, + ContentPacketExtension remoteContent, + CallPeerJabberImpl peer, + ColibriConferenceIQ.Channel channel) + throws OperationFailedException + { + if (remoteContent != null) + { + IceUdpTransportPacketExtension transport + = remoteContent.getFirstChildOfType( + IceUdpTransportPacketExtension.class); + + channel.setTransport( + TransportManagerJabberImpl.cloneTransportAndCandidates( + transport)); + } + } + + /** + * Makes an attempt to ensure that a specific + * ColibriConferenceIQ.Channel has a non-null + * transport set. If the specified channel does not have + * a transport, the method invokes the TransportManager of + * the specified CallPeerJabberImpl to initialize a new + * PacketExtension. + * + * @param channel the ColibriConferenceIQ.Channel to ensure the + * transport on + * @param peer the CallPeerJabberImpl which is associated with the + * specified channel and which specifies the + * TransportManager to be described in the specified + * channel + * @return the transport of the specified channel + */ + private IceUdpTransportPacketExtension ensureTransportOnChannel( + ColibriConferenceIQ.Channel channel, + CallPeerJabberImpl peer) + { + IceUdpTransportPacketExtension transport + = channel.getTransport(); + + if (transport == null) + { + PacketExtension pe + = peer + .getMediaHandler() + .getTransportManager() + .createTransportPacketExtension(); + + if (pe instanceof IceUdpTransportPacketExtension) + { + transport = (IceUdpTransportPacketExtension) pe; + channel.setTransport(transport); + } + } + return transport; + } + + /** + * Gets the entity ID of the Jitsi Videobridge to be utilized by this + * Call for the purposes of establishing a server-assisted + * telephony conference. + * + * @return the entity ID of the Jitsi Videobridge to be utilized by this + * Call for the purposes of establishing a server-assisted + * telephony conference. + */ + public String getJitsiVideobridge() + { + if ((this.jitsiVideobridge == null) + && getConference().isJitsiVideobridge()) + { + String jitsiVideobridge + = getProtocolProvider().getJitsiVideobridge(); + + if (jitsiVideobridge != null) + this.jitsiVideobridge = jitsiVideobridge; + } + return this.jitsiVideobridge; + } +} diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerJabberImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerJabberImpl.java index 110bb6ccc..389db8d91 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerJabberImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerJabberImpl.java @@ -1596,7 +1596,7 @@ else if (MediaType.VIDEO.equals(mediaType)) * MediaType * @return the MediaType of content. */ - public MediaType getMediaType (ContentPacketExtension content) + public MediaType getMediaType(ContentPacketExtension content) { String contentName = content.getName(); if (contentName == null) diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerMediaHandlerJabberImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerMediaHandlerJabberImpl.java index 6c28660e8..ffa4ce006 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerMediaHandlerJabberImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerMediaHandlerJabberImpl.java @@ -879,8 +879,6 @@ protected synchronized TransportManagerJabberImpl getTransportManager() = protocolProvider.getDiscoveryManager(); DiscoverInfo peerDiscoverInfo = peer.getDiscoveryInfo(); - boolean isJitsiVideobridge = peer.isJitsiVideobridge(); - /* * If this.supportedTransports has been explicitly set, we use * it to select the transport manager -- we use the first @@ -938,13 +936,40 @@ else if (ProtocolProviderServiceJabberImpl. }; /* - * If the local peer is a conference focus and there is a - * Jitsi Videobridge working on the server, prefer a - * transport which will route the conference through there. + * If Jitsi Videobridge is to be employed, pick up a Jingle + * transport supported by it. */ - if (isJitsiVideobridge) + if (peer.isJitsiVideobridge()) { - // TODO Auto-generated method stub + CallJabberImpl call = peer.getCall(); + + if (call != null) + { + String jitsiVideobridge + = peer.getCall().getJitsiVideobridge(); + + /* + * Jitsi Videobridge supports the Jingle Raw UDP + * transport from its inception. But that is not the + * case with the Jingle ICE-UDP transport. + */ + if ((jitsiVideobridge != null) + && !protocolProvider.isFeatureSupported( + jitsiVideobridge, + ProtocolProviderServiceJabberImpl + .URN_XMPP_JINGLE_ICE_UDP_1)) + { + for (int i = transports.length - 1; i >= 0; i--) + { + if (ProtocolProviderServiceJabberImpl + .URN_XMPP_JINGLE_ICE_UDP_1 + .equals(transports[i])) + { + transports[i] = null; + } + } + } + } } /* @@ -954,6 +979,8 @@ else if (ProtocolProviderServiceJabberImpl. */ for (String transport : transports) { + if (transport == null) + continue; if (isFeatureSupported( discoveryManager, peerDiscoverInfo, @@ -1961,8 +1988,8 @@ public void setRemotelyOnHold(boolean onHold) if (getPeer().isJitsiVideobridge()) { /* - * If we are the focus of a videobridge conference, we need to - * ask the videobridge to change the stream direction on our + * If we are the focus of a Videobridge conference, we need to + * ask the Videobridge to change the stream direction on our * behalf. */ ColibriConferenceIQ.Channel channel @@ -1983,7 +2010,7 @@ public void setRemotelyOnHold(boolean onHold) mediaType, direction); } - else //no videobridge + else //no Videobridge { if (remotelyOnHold) { @@ -2192,7 +2219,7 @@ private ColibriConferenceIQ.Channel getColibriChannel(MediaType mediaType) * {@inheritDoc} * * The super implementation relies on the direction of the streams and is - * therefore not accurate when we use a videobridge. + * therefore not accurate when we use a Videobridge. */ @Override public boolean isRemotelyOnHold() @@ -2203,7 +2230,7 @@ public boolean isRemotelyOnHold() /** * {@inheritDoc} * - * Handles the case when a videobridge is in use. + * Handles the case when a Videobridge is in use. * * @param locallyOnHold true if we are to make our streams * stop transmitting and false if we are to start transmitting diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/TransportManagerJabberImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/TransportManagerJabberImpl.java index 95e523419..48a8b1921 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/TransportManagerJabberImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/TransportManagerJabberImpl.java @@ -1,966 +1,1014 @@ -/* - * 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.net.*; -import java.util.*; - -import net.java.sip.communicator.impl.protocol.jabber.extensions.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*; -import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; -import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.service.protocol.media.*; -import net.java.sip.communicator.util.*; - -import org.jitsi.service.neomedia.*; -import org.jivesoftware.smack.packet.*; - -/** - * TransportManagers gather local candidates for incoming and outgoing - * calls. Their work starts by calling a start method which, using the remote - * peer's session description, would start the harvest. Calling a second wrapup - * method would deliver the candidate harvest, possibly after blocking if it has - * not yet completed. - * - * @author Emil Ivov - * @author Lyubomir Marinov - */ -public abstract class TransportManagerJabberImpl - extends TransportManager -{ - private static final Logger logger - = Logger.getLogger(TransportManagerJabberImpl.class); - - /** - * The ID that we will be assigning to our next candidate. We use - * ints for interoperability reasons (Emil: I believe that GTalk - * uses ints. If that turns out not to be the case we can stop - * using ints here if that's an issue). - */ - private static int nextID = 1; - - /** - * The information pertaining to the Jisti Videobridge conference which the - * local peer represented by this instance is a focus of. It gives a view of - * the whole Jitsi Videobridge conference managed by the associated - * CallJabberImpl which provides information specific to this - * TransportManager only. - */ - private ColibriConferenceIQ colibri; - - /** - * The generation of the candidates we are currently generating - */ - private int currentGeneration = 0; - - boolean isEstablishingConnectivityWithJitsiVideobridge = false; - - boolean startConnectivityEstablishmentWithJitsiVideobridge = false; - - /** - * 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. - */ - protected TransportManagerJabberImpl(CallPeerJabberImpl callPeer) - { - super(callPeer); - } - - /** - * Returns the InetAddress that is most likely to be to be used - * as a next hop when contacting the specified destination. This is - * an utility method that is used whenever we have to choose one of our - * local addresses to put in the Via, Contact or (in the case of no - * registrar accounts) From headers. - * - * @param peer the CallPeer that we would contact. - * - * @return the InetAddress that is most likely to be to be used - * as a next hop when contacting the specified destination. - * - * @throws IllegalArgumentException if destination is not a valid - * host/IP/FQDN - */ - @Override - protected InetAddress getIntendedDestination(CallPeerJabberImpl peer) - { - return peer.getProtocolProvider().getNextHop(); - } - - /** - * Returns the ID that we will be assigning to the next candidate we create. - * - * @return the next ID to use with a candidate. - */ - protected String getNextID() - { - int nextID; - - synchronized (TransportManagerJabberImpl.class) - { - nextID = TransportManagerJabberImpl.nextID++; - } - return Integer.toString(nextID); - } - - /** - * 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 - */ - public abstract MediaStreamTarget getStreamTarget(MediaType mediaType); - - /** - * Gets the XML namespace of the Jingle transport implemented by this - * TransportManagerJabberImpl. - * - * @return the XML namespace of the Jingle transport implemented by this - * TransportManagerJabberImpl - */ - public abstract String getXmlNamespace(); - - /** - * Returns the generation that our current candidates belong to. - * - * @return the generation that we should assign to candidates that we are - * currently advertising. - */ - protected int getCurrentGeneration() - { - return currentGeneration; - } - - /** - * Increments the generation that we are assigning candidates. - */ - protected void incrementGeneration() - { - currentGeneration++; - } - - protected void sendTransportInfoToJitsiVideobridge( - Map map) - { - CallPeerJabberImpl peer = getCallPeer(); - boolean initiator = !peer.isInitiator(); - ColibriConferenceIQ conferenceRequest = null; - - for (Map.Entry e - : map.entrySet()) - { - String media = e.getKey(); - MediaType mediaType = MediaType.parseString(media); - ColibriConferenceIQ.Channel channel - = getColibriChannel(mediaType, false /* remote */); - - if (channel != null) - { - IceUdpTransportPacketExtension transport; - - try - { - transport = cloneTransportAndCandidates(e.getValue()); - } - catch (OperationFailedException ofe) - { - transport = null; - } - if (transport == null) - continue; - - ColibriConferenceIQ.Channel channelRequest - = new ColibriConferenceIQ.Channel(); - - channelRequest.setID(channel.getID()); - channelRequest.setInitiator(initiator); - channelRequest.setTransport(transport); - - if (conferenceRequest == null) - { - if (colibri == null) - break; - else - { - String id = colibri.getID(); - - if ((id == null) || (id.length() == 0)) - break; - else - { - conferenceRequest = new ColibriConferenceIQ(); - conferenceRequest.setID(id); - conferenceRequest.setTo(colibri.getFrom()); - conferenceRequest.setType(IQ.Type.SET); - } - } - } - conferenceRequest.getOrCreateContent(media).addChannel( - channelRequest); - } - } - if (conferenceRequest != null) - { - peer.getProtocolProvider().getConnection().sendPacket( - conferenceRequest); - } - } - - /** - * Starts transport candidate harvest for a specific - * ContentPacketExtension that we are going to offer or answer - * with. - * - * @param theirContent the ContentPacketExtension offered by the - * remote peer to which we are going to answer with ourContent or - * null if ourContent will be an offer to the remote peer - * @param ourContent the ContentPacketExtension for which transport - * candidate harvest is to be started - * @param transportInfoSender a TransportInfoSender if the - * harvested transport candidates are to be sent in a - * transport-info rather than in ourContent; otherwise, - * null - * @param media the media of the RtpDescriptionPacketExtension - * child of ourContent - * @return a PacketExtension to be added as a child to - * ourContent; otherwise, null - * @throws OperationFailedException if anything goes wrong while starting - * transport candidate harvest for the specified ourContent - */ - protected abstract PacketExtension startCandidateHarvest( - ContentPacketExtension theirContent, - ContentPacketExtension ourContent, - TransportInfoSender transportInfoSender, - String media) - throws OperationFailedException; - - /** - * 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. - */ - public void startCandidateHarvest( - List theirOffer, - List ourAnswer, - TransportInfoSender transportInfoSender) - throws OperationFailedException - { - CallPeerJabberImpl peer = getCallPeer(); - CallJabberImpl call = peer.getCall(); - boolean isJitsiVideobridge = call.getConference().isJitsiVideobridge(); - List cpes - = (theirOffer == null) ? ourAnswer : theirOffer; - - /* - * If Jitsi Videobridge is to be used, determine which channels are to - * be allocated and attempt to allocate them now. - */ - if (isJitsiVideobridge) - { - Map contentMap - = new LinkedHashMap - (); - - for (ContentPacketExtension cpe : cpes) - { - RtpDescriptionPacketExtension rdpe - = cpe.getFirstChildOfType( - RtpDescriptionPacketExtension.class); - MediaType mediaType = MediaType.parseString(rdpe.getMedia()); - - /* - * The existence of a content for the mediaType and regardless - * of the existence of channels in it signals that a channel - * allocation request has already been sent for that mediaType. - */ - if ((colibri == null) - || (colibri.getContent(mediaType.toString()) == null)) - { - ContentPacketExtension local, remote; - - if (cpes == ourAnswer) - { - local = cpe; - remote - = (theirOffer == null) - ? null - : findContentByName(theirOffer, cpe.getName()); - } - else - { - local = findContentByName(ourAnswer, cpe.getName()); - remote = cpe; - } - contentMap.put(local, remote); - } - } - if (!contentMap.isEmpty()) - { - /* - * We are about to request the channel allocations for the media - * types found in contentMap. Regardless of the response, we do - * not want to repeat these requests. - */ - if (colibri == null) - colibri = new ColibriConferenceIQ(); - for (Map.Entry e - : contentMap.entrySet()) - { - ContentPacketExtension cpe = e.getValue(); - - if (cpe == null) - cpe = e.getKey(); - - RtpDescriptionPacketExtension rdpe - = cpe.getFirstChildOfType( - RtpDescriptionPacketExtension.class); - - colibri.getOrCreateContent(rdpe.getMedia()); - } - - ColibriConferenceIQ conferenceResult - = call.createColibriChannels(peer, contentMap); - - if (conferenceResult != null) - { - String videobridgeID = colibri.getID(); - String conferenceResultID = conferenceResult.getID(); - - if (videobridgeID == null) - colibri.setID(conferenceResultID); - else if (!videobridgeID.equals(conferenceResultID)) - throw new IllegalStateException("conference.id"); - - String videobridgeFrom = conferenceResult.getFrom(); - - if ((videobridgeFrom != null) - && (videobridgeFrom.length() != 0)) - { - colibri.setFrom(videobridgeFrom); - } - - for (ColibriConferenceIQ.Content contentResult - : conferenceResult.getContents()) - { - ColibriConferenceIQ.Content content - = colibri.getOrCreateContent( - contentResult.getName()); - - for (ColibriConferenceIQ.Channel channelResult - : contentResult.getChannels()) - { - if (content.getChannel(channelResult.getID()) - == null) - { - content.addChannel(channelResult); - } - } - } - } - else - { - /* - * The call fails if the createColibriChannels method fails - * which may happen if the conference packet times out or it - * can't be built. - */ - ProtocolProviderServiceJabberImpl - .throwOperationFailedException( - "Failed to allocate colibri channel.", - OperationFailedException.GENERAL_ERROR, - null, - logger); - } - } - } - - for (ContentPacketExtension cpe : cpes) - { - String contentName = cpe.getName(); - ContentPacketExtension ourContent - = findContentByName(ourAnswer, contentName); - - //it might be that we decided not to reply to this content - if (ourContent != null) - { - ContentPacketExtension theirContent - = (theirOffer == null) - ? null - : findContentByName(theirOffer, contentName); - RtpDescriptionPacketExtension rtpDesc - = ourContent.getFirstChildOfType( - RtpDescriptionPacketExtension.class); - String media = rtpDesc.getMedia(); - PacketExtension pe - = startCandidateHarvest( - theirContent, - ourContent, - transportInfoSender, - media); - - if (pe != null) - ourContent.addChildExtension(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 ourOffer 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. - */ - public void startCandidateHarvest( - List ourOffer, - TransportInfoSender transportInfoSender) - throws OperationFailedException - { - startCandidateHarvest( - /* theirOffer */ null, - ourOffer, - transportInfoSender); - } - - /** - * Notifies the transport manager that it should conclude candidate - * harvesting as soon as possible and return the lists of candidates - * gathered so far. - * - * @return the content list that we received earlier (possibly cloned into - * a new instance) and that we have updated with transport lists. - */ - public abstract List wrapupCandidateHarvest(); - - /** - * Looks through the cpExtList and returns the {@link - * ContentPacketExtension} with the specified name. - * - * @param cpExtList the list that we will be searching for a specific - * content. - * @param name the name of the content element we are looking for. - * @return the {@link ContentPacketExtension} with the specified name or - * null if no such content element exists. - */ - public static ContentPacketExtension findContentByName( - Iterable cpExtList, - String name) - { - for(ContentPacketExtension cpExt : cpExtList) - { - if(cpExt.getName().equals(name)) - return cpExt; - } - return null; - } - - /** - * Starts the connectivity establishment of this - * TransportManagerJabberImpl i.e. checks the connectivity between - * the local and the remote peers given the remote counterpart of the - * negotiation between them. - * - * @param remote the collection of ContentPacketExtensions which - * represents the remote counterpart of the negotiation between the local - * and the remote peer - * @return true if connectivity establishment has been started in - * response to the call; otherwise, false. - * TransportManagerJabberImpl implementations which do not perform - * connectivity checks (e.g. raw UDP) should return true. The - * default implementation does not perform connectivity checks and always - * returns true. - */ - public boolean startConnectivityEstablishment( - Iterable remote) - { - return true; - } - - /** - * Starts the connectivity establishment of this - * TransportManagerJabberImpl i.e. checks the connectivity between - * the local and the remote peers given the remote counterpart of the - * negotiation between them. - * - * @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. - * TransportManagerJabberImpl implementations which do not perform - * connectivity checks (e.g. raw UDP) should return true. The - * default implementation does not perform connectivity checks and always - * returns true. - */ - protected boolean startConnectivityEstablishment( - Map remote) - { - return true; - } - - /** - * Notifies this TransportManagerJabberImpl that it should conclude - * any started connectivity establishment. - * - * @throws OperationFailedException if anything goes wrong with connectivity - * establishment (i.e. ICE failed, ...) - */ - public void wrapupConnectivityEstablishment() - throws OperationFailedException - { - } - - /** - * 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. - *

- * Note: Because TransportManager deals with - * MediaTypes, not content names and - * TransportManagerJabberImpl does not implement translating from - * content name to MediaType, implementers are expected to call - * {@link TransportManager#closeStreamConnector(MediaType)}. - *

- * - * @param name the name of the content to be removed from the - * transport-related part of the session represented by this - * TransportManagerJabberImpl - */ - public abstract void removeContent(String name); - - /** - * Removes a content with a specific name from a specific collection of - * contents and closes any associated StreamConnector. - * - * @param contents the collection of contents to remove the content with the - * specified name from - * @param name the name of the content to remove - * @return the removed ContentPacketExtension if any; otherwise, - * null - */ - protected ContentPacketExtension removeContent( - Iterable contents, - String name) - { - for (Iterator contentIter = contents.iterator(); - contentIter.hasNext();) - { - ContentPacketExtension content = contentIter.next(); - - if (name.equals(content.getName())) - { - contentIter.remove(); - - // closeStreamConnector - RtpDescriptionPacketExtension rtpDescription - = content.getFirstChildOfType( - RtpDescriptionPacketExtension.class); - - if (rtpDescription != null) - { - closeStreamConnector( - MediaType.parseString(rtpDescription.getMedia())); - } - - return content; - } - } - return null; - } - - private static T clone(T src) - throws Exception - { - @SuppressWarnings("unchecked") - T dst = (T) src.getClass().newInstance(); - - // attributes - for (String name : src.getAttributeNames()) - dst.setAttribute(name, src.getAttribute(name)); - // namespace - dst.setNamespace(src.getNamespace()); - // text - dst.setText(src.getText()); - return dst; - } - - static IceUdpTransportPacketExtension cloneTransportAndCandidates( - IceUdpTransportPacketExtension src) - throws OperationFailedException - { - IceUdpTransportPacketExtension dst = null; - - if (src != null) - { - try - { - dst = clone(src); - - for (CandidatePacketExtension srcCand : src.getCandidateList()) - { - if (!(srcCand instanceof RemoteCandidatePacketExtension)) - dst.addCandidate(clone(srcCand)); - } - } - catch (Exception e) - { - dst = null; - ProtocolProviderServiceJabberImpl - .throwOperationFailedException( - "Failed to close transport and candidates.", - OperationFailedException.GENERAL_ERROR, - e, - logger); - } - } - return dst; - } - - /** - * Releases the resources acquired by this TransportManager and - * prepares it for garbage collection. - */ - public void close() - { - for (MediaType mediaType : MediaType.values()) - closeStreamConnector(mediaType); - } - - /** - * Closes a specific StreamConnector associated with a specific - * MediaType. If this TransportManager has a reference to - * the specified streamConnector, it remains. - * Also expires the ColibriConferenceIQ.Channel associated with - * the closed StreamConnector. - * - * @param mediaType the MediaType associated with the specified - * streamConnector - * @param streamConnector the StreamConnector to be closed - */ - @Override - protected void closeStreamConnector( - MediaType mediaType, - StreamConnector streamConnector) - { - try - { - boolean superCloseStreamConnector = true; - - if (streamConnector instanceof ColibriStreamConnector) - { - CallPeerJabberImpl peer = getCallPeer(); - - if (peer != null) - { - CallJabberImpl call = peer.getCall(); - - if (call != null) - { - superCloseStreamConnector = false; - call.closeColibriStreamConnector( - peer, - mediaType, - (ColibriStreamConnector) streamConnector); - } - } - } - if (superCloseStreamConnector) - super.closeStreamConnector(mediaType, streamConnector); - } - finally - { - /* - * Expire the ColibriConferenceIQ.Channel associated with the closed - * StreamConnector. - */ - if (colibri != null) - { - ColibriConferenceIQ.Content content - = colibri.getContent(mediaType.toString()); - - if (content != null) - { - List channels - = content.getChannels(); - - if (channels.size() == 2) - { - ColibriConferenceIQ requestConferenceIQ - = new ColibriConferenceIQ(); - - requestConferenceIQ.setID(colibri.getID()); - - ColibriConferenceIQ.Content requestContent - = requestConferenceIQ.getOrCreateContent( - content.getName()); - - requestContent.addChannel(channels.get(1 /* remote */)); - - /* - * Regardless of whether the request to expire the - * Channel associated with mediaType succeeds, consider - * the Channel in question expired. Since - * RawUdpTransportManager allocates a single channel per - * MediaType, consider the whole Content expired. - */ - colibri.removeContent(content); - - CallPeerJabberImpl peer = getCallPeer(); - - if (peer != null) - { - CallJabberImpl call = peer.getCall(); - - if (call != null) - { - call.expireColibriChannels( - peer, - requestConferenceIQ); - } - } - } - } - } - } - } - - /** - * {@inheritDoc} - * - * Adds support for telephony conferences utilizing the Jitsi Videobridge - * server-side technology. - * - * @see #doCreateStreamConnector(MediaType) - */ - @Override - protected StreamConnector createStreamConnector(final MediaType mediaType) - throws OperationFailedException - { - ColibriConferenceIQ.Channel channel - = getColibriChannel(mediaType, true /* local */); - - if (channel != null) - { - CallPeerJabberImpl peer = getCallPeer(); - CallJabberImpl call = peer.getCall(); - StreamConnector streamConnector - = call.createColibriStreamConnector( - peer, - mediaType, - channel, - new StreamConnectorFactory() - { - public StreamConnector createStreamConnector() - { - try - { - return doCreateStreamConnector(mediaType); - } - catch (OperationFailedException ofe) - { - return null; - } - } - }); - - if (streamConnector != null) - return streamConnector; - } - - return doCreateStreamConnector(mediaType); - } - - protected abstract PacketExtension createTransport(String media) - throws OperationFailedException; - - protected PacketExtension createTransportForStartCandidateHarvest( - String media) - throws OperationFailedException - { - PacketExtension pe = null; - - if (getCallPeer().isJitsiVideobridge()) - { - MediaType mediaType = MediaType.parseString(media); - ColibriConferenceIQ.Channel channel - = getColibriChannel(mediaType, false /* remote */); - - if (channel != null) - pe = cloneTransportAndCandidates(channel.getTransport()); - } - else - pe = createTransport(media); - return pe; - } - - /** - * Initializes a new PacketExtension instance appropriate to the - * type of Jingle transport represented by this TransportManager. - * The new instance is not initialized with any attributes or child - * extensions. - * - * @return a new PacketExtension instance appropriate to the type - * of Jingle transport represented by this TransportManager - */ - protected abstract PacketExtension createTransportPacketExtension(); - - /** - * Creates a media StreamConnector for a stream of a specific - * MediaType. The minimum and maximum of the media port boundaries - * are taken into account. - * - * @param mediaType the MediaType of the stream for which a - * StreamConnector is to be created - * @return a StreamConnector for the stream of the specified - * mediaType - * @throws OperationFailedException if the binding of the sockets fails - */ - protected StreamConnector doCreateStreamConnector(MediaType mediaType) - throws OperationFailedException - { - return super.createStreamConnector(mediaType); - } - - /** - * Finds a TransportManagerJabberImpl participating in a telephony - * conference utilizing the Jitsi Videobridge server-side technology that - * this instance is participating in which is establishing the connectivity - * with the Jitsi Videobridge server (as opposed to a CallPeer). - * - * @return a TransportManagerJabberImpl which is participating in - * a telephony conference utilizing the Jitsi Videobridge server-side - * technology that this instance is participating in which is establishing - * the connectivity with the Jitsi Videobridge server (as opposed to a - * CallPeer). - */ - TransportManagerJabberImpl - findTransportManagerEstablishingConnectivityWithJitsiVideobridge() - { - Call call = getCallPeer().getCall(); - TransportManagerJabberImpl transportManager = null; - - if (call != null) - { - CallConference conference = call.getConference(); - - if ((conference != null) && conference.isJitsiVideobridge()) - { - for (Call aCall : conference.getCalls()) - { - Iterator callPeerIter - = aCall.getCallPeers(); - - while (callPeerIter.hasNext()) - { - CallPeer aCallPeer = callPeerIter.next(); - - if (aCallPeer instanceof CallPeerJabberImpl) - { - TransportManagerJabberImpl aTransportManager - = ((CallPeerJabberImpl) aCallPeer) - .getMediaHandler() - .getTransportManager(); - - if (aTransportManager - .isEstablishingConnectivityWithJitsiVideobridge) - { - transportManager = aTransportManager; - break; - } - } - } - } - } - } - return transportManager; - } - - /** - * Gets the {@link ColibriConferenceIQ.Channel} which belongs to a content - * associated with a specific MediaType and is to be either locally - * or remotely used. - *

- * Note: Modifications to the ColibriConferenceIQ.Channel - * instance returned by the method propagate to (the state of) this - * instance. - *

- * - * @param mediaType the MediaType associated with the content which - * contains the ColibriConferenceIQ.Channel to get - * @param local true if the ColibriConferenceIQ.Channel - * which is to be used locally is to be returned or false for the - * one which is to be used remotely - * @return the ColibriConferenceIQ.Channel which belongs to a - * content associated with the specified mediaType and which is to - * be used in accord with the specified local indicator if such a - * channel exists; otherwise, null - */ - ColibriConferenceIQ.Channel getColibriChannel( - MediaType mediaType, - boolean local) - { - ColibriConferenceIQ.Channel channel = null; - - if (colibri != null) - { - ColibriConferenceIQ.Content content - = colibri.getContent(mediaType.toString()); - - if (content != null) - { - List channels - = content.getChannels(); - - if (channels.size() == 2) - channel = channels.get(local ? 0 : 1); - } - } - return channel; - } -} +/* + * 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.net.*; +import java.util.*; + +import net.java.sip.communicator.impl.protocol.jabber.extensions.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; +import net.java.sip.communicator.service.protocol.*; +import net.java.sip.communicator.service.protocol.media.*; +import net.java.sip.communicator.util.*; + +import org.jitsi.service.neomedia.*; +import org.jivesoftware.smack.packet.*; + +/** + * TransportManagers gather local candidates for incoming and outgoing + * calls. Their work starts by calling a start method which, using the remote + * peer's session description, would start the harvest. Calling a second wrapup + * method would deliver the candidate harvest, possibly after blocking if it has + * not yet completed. + * + * @author Emil Ivov + * @author Lyubomir Marinov + */ +public abstract class TransportManagerJabberImpl + extends TransportManager +{ + /** + * The Logger used by the TransportManagerJabberImpl class + * and its instances to print debug messages. + */ + private static final Logger logger + = Logger.getLogger(TransportManagerJabberImpl.class); + + /** + * The ID that we will be assigning to our next candidate. We use + * ints for interoperability reasons (Emil: I believe that GTalk + * uses ints. If that turns out not to be the case we can stop + * using ints here if that's an issue). + */ + private static int nextID = 1; + + /** + * The information pertaining to the Jisti Videobridge conference which the + * local peer represented by this instance is a focus of. It gives a view of + * the whole Jitsi Videobridge conference managed by the associated + * CallJabberImpl which provides information specific to this + * TransportManager only. + */ + private ColibriConferenceIQ colibri; + + /** + * The generation of the candidates we are currently generating + */ + private int currentGeneration = 0; + + /** + * The indicator which determines whether this TransportManager + * instance is responsible to establish the connectivity with the associated + * Jitsi Videobridge (in case it is being employed at all). + */ + boolean isEstablishingConnectivityWithJitsiVideobridge = false; + + /** + * The indicator which determines whether this TransportManager + * instance is yet to start establishing the connectivity with the + * associated Jitsi Videobridge (in case it is being employed at all). + */ + boolean startConnectivityEstablishmentWithJitsiVideobridge = false; + + /** + * 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. + */ + protected TransportManagerJabberImpl(CallPeerJabberImpl callPeer) + { + super(callPeer); + } + + /** + * Returns the InetAddress that is most likely to be to be used + * as a next hop when contacting the specified destination. This is + * an utility method that is used whenever we have to choose one of our + * local addresses to put in the Via, Contact or (in the case of no + * registrar accounts) From headers. + * + * @param peer the CallPeer that we would contact. + * + * @return the InetAddress that is most likely to be to be used + * as a next hop when contacting the specified destination. + * + * @throws IllegalArgumentException if destination is not a valid + * host/IP/FQDN + */ + @Override + protected InetAddress getIntendedDestination(CallPeerJabberImpl peer) + { + return peer.getProtocolProvider().getNextHop(); + } + + /** + * Returns the ID that we will be assigning to the next candidate we create. + * + * @return the next ID to use with a candidate. + */ + protected String getNextID() + { + int nextID; + + synchronized (TransportManagerJabberImpl.class) + { + nextID = TransportManagerJabberImpl.nextID++; + } + return Integer.toString(nextID); + } + + /** + * 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 + */ + public abstract MediaStreamTarget getStreamTarget(MediaType mediaType); + + /** + * Gets the XML namespace of the Jingle transport implemented by this + * TransportManagerJabberImpl. + * + * @return the XML namespace of the Jingle transport implemented by this + * TransportManagerJabberImpl + */ + public abstract String getXmlNamespace(); + + /** + * Returns the generation that our current candidates belong to. + * + * @return the generation that we should assign to candidates that we are + * currently advertising. + */ + protected int getCurrentGeneration() + { + return currentGeneration; + } + + /** + * Increments the generation that we are assigning candidates. + */ + protected void incrementGeneration() + { + currentGeneration++; + } + + /** + * Sends transport-related information received from the remote peer to the + * associated Jiitsi Videobridge in order to update the (remote) + * ColibriConferenceIQ.Channel associated with this + * TransportManager instance. + * + * @param map a Map of media-IceUdpTransportPacketExtension pairs + * which represents the transport-related information which has been + * received from the remote peer and which is to be sent to the associated + * Jitsi Videobridge + */ + protected void sendTransportInfoToJitsiVideobridge( + Map map) + { + CallPeerJabberImpl peer = getCallPeer(); + boolean initiator = !peer.isInitiator(); + ColibriConferenceIQ conferenceRequest = null; + + for (Map.Entry e + : map.entrySet()) + { + String media = e.getKey(); + MediaType mediaType = MediaType.parseString(media); + ColibriConferenceIQ.Channel channel + = getColibriChannel(mediaType, false /* remote */); + + if (channel != null) + { + IceUdpTransportPacketExtension transport; + + try + { + transport = cloneTransportAndCandidates(e.getValue()); + } + catch (OperationFailedException ofe) + { + transport = null; + } + if (transport == null) + continue; + + ColibriConferenceIQ.Channel channelRequest + = new ColibriConferenceIQ.Channel(); + + channelRequest.setID(channel.getID()); + channelRequest.setInitiator(initiator); + channelRequest.setTransport(transport); + + if (conferenceRequest == null) + { + if (colibri == null) + break; + else + { + String id = colibri.getID(); + + if ((id == null) || (id.length() == 0)) + break; + else + { + conferenceRequest = new ColibriConferenceIQ(); + conferenceRequest.setID(id); + conferenceRequest.setTo(colibri.getFrom()); + conferenceRequest.setType(IQ.Type.SET); + } + } + } + conferenceRequest.getOrCreateContent(media).addChannel( + channelRequest); + } + } + if (conferenceRequest != null) + { + peer.getProtocolProvider().getConnection().sendPacket( + conferenceRequest); + } + } + + /** + * Starts transport candidate harvest for a specific + * ContentPacketExtension that we are going to offer or answer + * with. + * + * @param theirContent the ContentPacketExtension offered by the + * remote peer to which we are going to answer with ourContent or + * null if ourContent will be an offer to the remote peer + * @param ourContent the ContentPacketExtension for which transport + * candidate harvest is to be started + * @param transportInfoSender a TransportInfoSender if the + * harvested transport candidates are to be sent in a + * transport-info rather than in ourContent; otherwise, + * null + * @param media the media of the RtpDescriptionPacketExtension + * child of ourContent + * @return a PacketExtension to be added as a child to + * ourContent; otherwise, null + * @throws OperationFailedException if anything goes wrong while starting + * transport candidate harvest for the specified ourContent + */ + protected abstract PacketExtension startCandidateHarvest( + ContentPacketExtension theirContent, + ContentPacketExtension ourContent, + TransportInfoSender transportInfoSender, + String media) + throws OperationFailedException; + + /** + * 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. + */ + public void startCandidateHarvest( + List theirOffer, + List ourAnswer, + TransportInfoSender transportInfoSender) + throws OperationFailedException + { + CallPeerJabberImpl peer = getCallPeer(); + CallJabberImpl call = peer.getCall(); + boolean isJitsiVideobridge = call.getConference().isJitsiVideobridge(); + List cpes + = (theirOffer == null) ? ourAnswer : theirOffer; + + /* + * If Jitsi Videobridge is to be used, determine which channels are to + * be allocated and attempt to allocate them now. + */ + if (isJitsiVideobridge) + { + Map contentMap + = new LinkedHashMap + (); + + for (ContentPacketExtension cpe : cpes) + { + RtpDescriptionPacketExtension rdpe + = cpe.getFirstChildOfType( + RtpDescriptionPacketExtension.class); + MediaType mediaType = MediaType.parseString(rdpe.getMedia()); + + /* + * The existence of a content for the mediaType and regardless + * of the existence of channels in it signals that a channel + * allocation request has already been sent for that mediaType. + */ + if ((colibri == null) + || (colibri.getContent(mediaType.toString()) == null)) + { + ContentPacketExtension local, remote; + + if (cpes == ourAnswer) + { + local = cpe; + remote + = (theirOffer == null) + ? null + : findContentByName(theirOffer, cpe.getName()); + } + else + { + local = findContentByName(ourAnswer, cpe.getName()); + remote = cpe; + } + contentMap.put(local, remote); + } + } + if (!contentMap.isEmpty()) + { + /* + * We are about to request the channel allocations for the media + * types found in contentMap. Regardless of the response, we do + * not want to repeat these requests. + */ + if (colibri == null) + colibri = new ColibriConferenceIQ(); + for (Map.Entry e + : contentMap.entrySet()) + { + ContentPacketExtension cpe = e.getValue(); + + if (cpe == null) + cpe = e.getKey(); + + RtpDescriptionPacketExtension rdpe + = cpe.getFirstChildOfType( + RtpDescriptionPacketExtension.class); + + colibri.getOrCreateContent(rdpe.getMedia()); + } + + ColibriConferenceIQ conferenceResult + = call.createColibriChannels(peer, contentMap); + + if (conferenceResult != null) + { + String videobridgeID = colibri.getID(); + String conferenceResultID = conferenceResult.getID(); + + if (videobridgeID == null) + colibri.setID(conferenceResultID); + else if (!videobridgeID.equals(conferenceResultID)) + throw new IllegalStateException("conference.id"); + + String videobridgeFrom = conferenceResult.getFrom(); + + if ((videobridgeFrom != null) + && (videobridgeFrom.length() != 0)) + { + colibri.setFrom(videobridgeFrom); + } + + for (ColibriConferenceIQ.Content contentResult + : conferenceResult.getContents()) + { + ColibriConferenceIQ.Content content + = colibri.getOrCreateContent( + contentResult.getName()); + + for (ColibriConferenceIQ.Channel channelResult + : contentResult.getChannels()) + { + if (content.getChannel(channelResult.getID()) + == null) + { + content.addChannel(channelResult); + } + } + } + } + else + { + /* + * The call fails if the createColibriChannels method fails + * which may happen if the conference packet times out or it + * can't be built. + */ + ProtocolProviderServiceJabberImpl + .throwOperationFailedException( + "Failed to allocate colibri channel.", + OperationFailedException.GENERAL_ERROR, + null, + logger); + } + } + } + + for (ContentPacketExtension cpe : cpes) + { + String contentName = cpe.getName(); + ContentPacketExtension ourContent + = findContentByName(ourAnswer, contentName); + + //it might be that we decided not to reply to this content + if (ourContent != null) + { + ContentPacketExtension theirContent + = (theirOffer == null) + ? null + : findContentByName(theirOffer, contentName); + RtpDescriptionPacketExtension rtpDesc + = ourContent.getFirstChildOfType( + RtpDescriptionPacketExtension.class); + String media = rtpDesc.getMedia(); + PacketExtension pe + = startCandidateHarvest( + theirContent, + ourContent, + transportInfoSender, + media); + + if (pe != null) + ourContent.addChildExtension(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 ourOffer 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. + */ + public void startCandidateHarvest( + List ourOffer, + TransportInfoSender transportInfoSender) + throws OperationFailedException + { + startCandidateHarvest( + /* theirOffer */ null, + ourOffer, + transportInfoSender); + } + + /** + * Notifies the transport manager that it should conclude candidate + * harvesting as soon as possible and return the lists of candidates + * gathered so far. + * + * @return the content list that we received earlier (possibly cloned into + * a new instance) and that we have updated with transport lists. + */ + public abstract List wrapupCandidateHarvest(); + + /** + * Looks through the cpExtList and returns the {@link + * ContentPacketExtension} with the specified name. + * + * @param cpExtList the list that we will be searching for a specific + * content. + * @param name the name of the content element we are looking for. + * @return the {@link ContentPacketExtension} with the specified name or + * null if no such content element exists. + */ + public static ContentPacketExtension findContentByName( + Iterable cpExtList, + String name) + { + for(ContentPacketExtension cpExt : cpExtList) + { + if(cpExt.getName().equals(name)) + return cpExt; + } + return null; + } + + /** + * Starts the connectivity establishment of this + * TransportManagerJabberImpl i.e. checks the connectivity between + * the local and the remote peers given the remote counterpart of the + * negotiation between them. + * + * @param remote the collection of ContentPacketExtensions which + * represents the remote counterpart of the negotiation between the local + * and the remote peer + * @return true if connectivity establishment has been started in + * response to the call; otherwise, false. + * TransportManagerJabberImpl implementations which do not perform + * connectivity checks (e.g. raw UDP) should return true. The + * default implementation does not perform connectivity checks and always + * returns true. + */ + public boolean startConnectivityEstablishment( + Iterable remote) + { + return true; + } + + /** + * Starts the connectivity establishment of this + * TransportManagerJabberImpl i.e. checks the connectivity between + * the local and the remote peers given the remote counterpart of the + * negotiation between them. + * + * @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. + * TransportManagerJabberImpl implementations which do not perform + * connectivity checks (e.g. raw UDP) should return true. The + * default implementation does not perform connectivity checks and always + * returns true. + */ + protected boolean startConnectivityEstablishment( + Map remote) + { + return true; + } + + /** + * Notifies this TransportManagerJabberImpl that it should conclude + * any started connectivity establishment. + * + * @throws OperationFailedException if anything goes wrong with connectivity + * establishment (i.e. ICE failed, ...) + */ + public void wrapupConnectivityEstablishment() + throws OperationFailedException + { + } + + /** + * 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. + *

+ * Note: Because TransportManager deals with + * MediaTypes, not content names and + * TransportManagerJabberImpl does not implement translating from + * content name to MediaType, implementers are expected to call + * {@link TransportManager#closeStreamConnector(MediaType)}. + *

+ * + * @param name the name of the content to be removed from the + * transport-related part of the session represented by this + * TransportManagerJabberImpl + */ + public abstract void removeContent(String name); + + /** + * Removes a content with a specific name from a specific collection of + * contents and closes any associated StreamConnector. + * + * @param contents the collection of contents to remove the content with the + * specified name from + * @param name the name of the content to remove + * @return the removed ContentPacketExtension if any; otherwise, + * null + */ + protected ContentPacketExtension removeContent( + Iterable contents, + String name) + { + for (Iterator contentIter = contents.iterator(); + contentIter.hasNext();) + { + ContentPacketExtension content = contentIter.next(); + + if (name.equals(content.getName())) + { + contentIter.remove(); + + // closeStreamConnector + RtpDescriptionPacketExtension rtpDescription + = content.getFirstChildOfType( + RtpDescriptionPacketExtension.class); + + if (rtpDescription != null) + { + closeStreamConnector( + MediaType.parseString(rtpDescription.getMedia())); + } + + return content; + } + } + return null; + } + + /** + * Clones the attributes, namespace and text of a specific + * AbstractPacketExtension into a new + * AbstractPacketExtension instance of the same run-time type. + * + * @param src the AbstractPacketExtension to be cloned + * @return a new AbstractPacketExtension instance of the run-time + * type of the specified src which has the same attributes, + * namespace and text + * @throws Exception if an error occurs during the cloning of the specified + * src + */ + private static T clone(T src) + throws Exception + { + @SuppressWarnings("unchecked") + T dst = (T) src.getClass().newInstance(); + + // attributes + for (String name : src.getAttributeNames()) + dst.setAttribute(name, src.getAttribute(name)); + // namespace + dst.setNamespace(src.getNamespace()); + // text + dst.setText(src.getText()); + return dst; + } + + /** + * Clones a specific IceUdpTransportPacketExtension and its + * candidates. + * + * @param src the IceUdpTransportPacketExtension to be cloned + * @return a new IceUdpTransportPacketExtension instance which has + * the same run-time type, attributes, namespace, text and candidates as the + * specified src + * @throws OperationFailedException if an error occurs during the cloing of + * the specified src and its candidates + */ + static IceUdpTransportPacketExtension cloneTransportAndCandidates( + IceUdpTransportPacketExtension src) + throws OperationFailedException + { + IceUdpTransportPacketExtension dst = null; + + if (src != null) + { + try + { + dst = clone(src); + + for (CandidatePacketExtension srcCand : src.getCandidateList()) + { + if (!(srcCand instanceof RemoteCandidatePacketExtension)) + dst.addCandidate(clone(srcCand)); + } + } + catch (Exception e) + { + dst = null; + ProtocolProviderServiceJabberImpl + .throwOperationFailedException( + "Failed to close transport and candidates.", + OperationFailedException.GENERAL_ERROR, + e, + logger); + } + } + return dst; + } + + /** + * Releases the resources acquired by this TransportManager and + * prepares it for garbage collection. + */ + public void close() + { + for (MediaType mediaType : MediaType.values()) + closeStreamConnector(mediaType); + } + + /** + * Closes a specific StreamConnector associated with a specific + * MediaType. If this TransportManager has a reference to + * the specified streamConnector, it remains. + * Also expires the ColibriConferenceIQ.Channel associated with + * the closed StreamConnector. + * + * @param mediaType the MediaType associated with the specified + * streamConnector + * @param streamConnector the StreamConnector to be closed + */ + @Override + protected void closeStreamConnector( + MediaType mediaType, + StreamConnector streamConnector) + { + try + { + boolean superCloseStreamConnector = true; + + if (streamConnector instanceof ColibriStreamConnector) + { + CallPeerJabberImpl peer = getCallPeer(); + + if (peer != null) + { + CallJabberImpl call = peer.getCall(); + + if (call != null) + { + superCloseStreamConnector = false; + call.closeColibriStreamConnector( + peer, + mediaType, + (ColibriStreamConnector) streamConnector); + } + } + } + if (superCloseStreamConnector) + super.closeStreamConnector(mediaType, streamConnector); + } + finally + { + /* + * Expire the ColibriConferenceIQ.Channel associated with the closed + * StreamConnector. + */ + if (colibri != null) + { + ColibriConferenceIQ.Content content + = colibri.getContent(mediaType.toString()); + + if (content != null) + { + List channels + = content.getChannels(); + + if (channels.size() == 2) + { + ColibriConferenceIQ requestConferenceIQ + = new ColibriConferenceIQ(); + + requestConferenceIQ.setID(colibri.getID()); + + ColibriConferenceIQ.Content requestContent + = requestConferenceIQ.getOrCreateContent( + content.getName()); + + requestContent.addChannel(channels.get(1 /* remote */)); + + /* + * Regardless of whether the request to expire the + * Channel associated with mediaType succeeds, consider + * the Channel in question expired. Since + * RawUdpTransportManager allocates a single channel per + * MediaType, consider the whole Content expired. + */ + colibri.removeContent(content); + + CallPeerJabberImpl peer = getCallPeer(); + + if (peer != null) + { + CallJabberImpl call = peer.getCall(); + + if (call != null) + { + call.expireColibriChannels( + peer, + requestConferenceIQ); + } + } + } + } + } + } + } + + /** + * {@inheritDoc} + * + * Adds support for telephony conferences utilizing the Jitsi Videobridge + * server-side technology. + * + * @see #doCreateStreamConnector(MediaType) + */ + @Override + protected StreamConnector createStreamConnector(final MediaType mediaType) + throws OperationFailedException + { + ColibriConferenceIQ.Channel channel + = getColibriChannel(mediaType, true /* local */); + + if (channel != null) + { + CallPeerJabberImpl peer = getCallPeer(); + CallJabberImpl call = peer.getCall(); + StreamConnector streamConnector + = call.createColibriStreamConnector( + peer, + mediaType, + channel, + new StreamConnectorFactory() + { + public StreamConnector createStreamConnector() + { + try + { + return doCreateStreamConnector(mediaType); + } + catch (OperationFailedException ofe) + { + return null; + } + } + }); + + if (streamConnector != null) + return streamConnector; + } + + return doCreateStreamConnector(mediaType); + } + + protected abstract PacketExtension createTransport(String media) + throws OperationFailedException; + + protected PacketExtension createTransportForStartCandidateHarvest( + String media) + throws OperationFailedException + { + PacketExtension pe = null; + + if (getCallPeer().isJitsiVideobridge()) + { + MediaType mediaType = MediaType.parseString(media); + ColibriConferenceIQ.Channel channel + = getColibriChannel(mediaType, false /* remote */); + + if (channel != null) + pe = cloneTransportAndCandidates(channel.getTransport()); + } + else + pe = createTransport(media); + return pe; + } + + /** + * Initializes a new PacketExtension instance appropriate to the + * type of Jingle transport represented by this TransportManager. + * The new instance is not initialized with any attributes or child + * extensions. + * + * @return a new PacketExtension instance appropriate to the type + * of Jingle transport represented by this TransportManager + */ + protected abstract PacketExtension createTransportPacketExtension(); + + /** + * Creates a media StreamConnector for a stream of a specific + * MediaType. The minimum and maximum of the media port boundaries + * are taken into account. + * + * @param mediaType the MediaType of the stream for which a + * StreamConnector is to be created + * @return a StreamConnector for the stream of the specified + * mediaType + * @throws OperationFailedException if the binding of the sockets fails + */ + protected StreamConnector doCreateStreamConnector(MediaType mediaType) + throws OperationFailedException + { + return super.createStreamConnector(mediaType); + } + + /** + * Finds a TransportManagerJabberImpl participating in a telephony + * conference utilizing the Jitsi Videobridge server-side technology that + * this instance is participating in which is establishing the connectivity + * with the Jitsi Videobridge server (as opposed to a CallPeer). + * + * @return a TransportManagerJabberImpl which is participating in + * a telephony conference utilizing the Jitsi Videobridge server-side + * technology that this instance is participating in which is establishing + * the connectivity with the Jitsi Videobridge server (as opposed to a + * CallPeer). + */ + TransportManagerJabberImpl + findTransportManagerEstablishingConnectivityWithJitsiVideobridge() + { + Call call = getCallPeer().getCall(); + TransportManagerJabberImpl transportManager = null; + + if (call != null) + { + CallConference conference = call.getConference(); + + if ((conference != null) && conference.isJitsiVideobridge()) + { + for (Call aCall : conference.getCalls()) + { + Iterator callPeerIter + = aCall.getCallPeers(); + + while (callPeerIter.hasNext()) + { + CallPeer aCallPeer = callPeerIter.next(); + + if (aCallPeer instanceof CallPeerJabberImpl) + { + TransportManagerJabberImpl aTransportManager + = ((CallPeerJabberImpl) aCallPeer) + .getMediaHandler() + .getTransportManager(); + + if (aTransportManager + .isEstablishingConnectivityWithJitsiVideobridge) + { + transportManager = aTransportManager; + break; + } + } + } + } + } + } + return transportManager; + } + + /** + * Gets the {@link ColibriConferenceIQ.Channel} which belongs to a content + * associated with a specific MediaType and is to be either locally + * or remotely used. + *

+ * Note: Modifications to the ColibriConferenceIQ.Channel + * instance returned by the method propagate to (the state of) this + * instance. + *

+ * + * @param mediaType the MediaType associated with the content which + * contains the ColibriConferenceIQ.Channel to get + * @param local true if the ColibriConferenceIQ.Channel + * which is to be used locally is to be returned or false for the + * one which is to be used remotely + * @return the ColibriConferenceIQ.Channel which belongs to a + * content associated with the specified mediaType and which is to + * be used in accord with the specified local indicator if such a + * channel exists; otherwise, null + */ + ColibriConferenceIQ.Channel getColibriChannel( + MediaType mediaType, + boolean local) + { + ColibriConferenceIQ.Channel channel = null; + + if (colibri != null) + { + ColibriConferenceIQ.Content content + = colibri.getContent(mediaType.toString()); + + if (content != null) + { + List channels + = content.getChannels(); + + if (channels.size() == 2) + channel = channels.get(local ? 0 : 1); + } + } + return channel; + } +}