diff options
author | Sebastien Vincent <seb@jitsi.org> | 2011-05-20 15:26:33 +0000 |
---|---|---|
committer | Sebastien Vincent <seb@jitsi.org> | 2011-05-20 15:26:33 +0000 |
commit | c65785209f07c9dde145b3dfa305783821d043d6 (patch) | |
tree | 21f3658816fdf4bfdd4240f614889051b4f17a11 /src/net/java/sip | |
parent | 400f019e34c76aacfb0621cabe1959b0606e87b9 (diff) | |
download | jitsi-c65785209f07c9dde145b3dfa305783821d043d6.zip jitsi-c65785209f07c9dde145b3dfa305783821d043d6.tar.gz jitsi-c65785209f07c9dde145b3dfa305783821d043d6.tar.bz2 |
Commits work in progress on Google Talk voice support.
Diffstat (limited to 'src/net/java/sip')
13 files changed, 2596 insertions, 15 deletions
diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/ActiveCallsRepositoryGTalkImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/ActiveCallsRepositoryGTalkImpl.java new file mode 100644 index 0000000..5c421b0 --- /dev/null +++ b/src/net/java/sip/communicator/impl/protocol/jabber/ActiveCallsRepositoryGTalkImpl.java @@ -0,0 +1,123 @@ +/* + * 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.util.*; + +import net.java.sip.communicator.service.protocol.*; + +/** + * Keeps a list of all calls currently active and maintained by this protocol + * provider. Offers methods for finding a call by its ID, peer session + * and others. + * + * @author Emil Ivov + * @author Symphorien Wanko + */ +public class ActiveCallsRepositoryGTalkImpl extends ActiveCallsRepository< + CallGTalkImpl, + OperationSetBasicTelephonyJabberImpl> +{ + /** + * It's where we store all active calls + * + * @param opSet the <tt>OperationSetBasicTelphony</tt> instance which has + * been used to create calls in this repository + */ + public ActiveCallsRepositoryGTalkImpl( + OperationSetBasicTelephonyJabberImpl opSet) + { + super(opSet); + } + + /** + * Returns the {@link CallGTalkImpl} containing a {@link + * CallPeerGTalkImpl} whose corresponding jingle session has the specified + * jingle <tt>sid</tt>. + * + * @param sid the jingle <tt>sid</tt> we're looking for. + * + * @return the {@link CallGTalkImpl} containing the peer with the + * specified <tt>sid</tt> or <tt>null</tt> if we couldn't find one matching + * it. + */ + public CallGTalkImpl findSessionID(String sid) + { + Iterator<CallGTalkImpl> calls = getActiveCalls(); + + while (calls.hasNext()) + { + CallGTalkImpl call = calls.next(); + if (call.containsSessionID(sid)) + return call; + } + + return null; + } + + /** + * Returns the {@link CallPeerGTalkImpl} whose jingle session has the + * specified jingle <tt>sid</tt>. + * + * @param sid the jingle <tt>sid</tt> we're looking for. + * + * @return the {@link CallPeerGTalkImpl} with the specified <tt>sid</tt> + * or tt>null</tt> if we couldn't find one matching it. + */ + public CallPeerGTalkImpl findCallPeer(String sid) + { + Iterator<CallGTalkImpl> calls = getActiveCalls(); + + while (calls.hasNext()) + { + CallGTalkImpl call = calls.next(); + CallPeerGTalkImpl peer = call.getPeer(sid); + if ( peer != null ) + return peer; + } + + return null; + } + + /** + * Returns the {@link CallPeerGTalkImpl} whose session-init's ID has + * the specified IQ <tt>id</tt>. + * + * @param id the IQ <tt>id</tt> we're looking for. + * + * @return the {@link CallPeerGTalkImpl} with the specified <tt>id</tt> + * or <tt>null</tt> if we couldn't find one matching it. + */ + public CallPeerGTalkImpl findCallPeerBySessInitPacketID(String id) + { + Iterator<CallGTalkImpl> calls = getActiveCalls(); + + while (calls.hasNext()) + { + CallGTalkImpl call = calls.next(); + CallPeerGTalkImpl peer = call.getPeerBySessInitPacketID(id); + if ( peer != null ) + return peer; + } + + return null; + } + + /** + * Creates and dispatches a <tt>CallEvent</tt> notifying registered + * listeners that an event with id <tt>eventID</tt> has occurred on + * <tt>sourceCall</tt>. + * + * @param eventID the ID of the event to dispatch + * @param sourceCall the call on which the event has occurred + * @see ActiveCallsRepository#fireCallEvent(int, Call) + */ + protected void fireCallEvent(int eventID, Call sourceCall) + { + parentOperationSet.fireCallEvent(eventID, sourceCall); + } +}
\ No newline at end of file diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/ActiveCallsRepositoryJabberImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/ActiveCallsRepositoryJabberImpl.java index 9733d3e..099c213 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/ActiveCallsRepositoryJabberImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/ActiveCallsRepositoryJabberImpl.java @@ -9,7 +9,6 @@ package net.java.sip.communicator.impl.protocol.jabber; import java.util.*; import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.util.*; /** * Keeps a list of all calls currently active and maintained by this protocol @@ -24,13 +23,6 @@ public class ActiveCallsRepositoryJabberImpl OperationSetBasicTelephonyJabberImpl> { /** - * The <tt>Logger</tt> used by the <tt>ActiveCallsRepositoryJabberImpl</tt> - * class and its instances for logging output. - */ - private static final Logger logger - = Logger.getLogger(ActiveCallsRepositoryJabberImpl.class); - - /** * It's where we store all active calls * * @param opSet the <tt>OperationSetBasicTelphony</tt> instance which has diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/CallGTalkImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/CallGTalkImpl.java new file mode 100644 index 0000000..eb34d0a --- /dev/null +++ b/src/net/java/sip/communicator/impl/protocol/jabber/CallGTalkImpl.java @@ -0,0 +1,184 @@ +/* + * 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 org.jivesoftware.smack.packet.*; + +import net.java.sip.communicator.impl.protocol.jabber.extensions.gtalk.*; +import net.java.sip.communicator.service.protocol.*; +import net.java.sip.communicator.service.protocol.event.*; +import net.java.sip.communicator.service.protocol.media.*; + +/** + * A Google Talk implementation of the <tt>Call</tt> abstract class + * encapsulating Google Talk sessions. + * + * @author Sebastien Vincent + */ +public class CallGTalkImpl + extends MediaAwareCall< + CallPeerGTalkImpl, + OperationSetBasicTelephonyJabberImpl, + ProtocolProviderServiceJabberImpl> +{ + /** + * Initializes a new <tt>CallGTalkImpl</tt> instance belonging to + * <tt>sourceProvider</tt> and associated with the jingle session with the + * specified <tt>sessionID</tt>. If the new instance corresponds to an + * incoming Google Talk session, then the sessionID would come from there. + * Otherwise, one could generate one using {@link SessionIQ#generateSID()}. + * + * @param parentOpSet the {@link OperationSetBasicTelephonyJabberImpl} + * instance in the context of which this call has been created. + */ + protected CallGTalkImpl( + OperationSetBasicTelephonyJabberImpl parentOpSet) + { + super(parentOpSet); + + //let's add ourselves to the calls repo. we are doing it ourselves just + //to make sure that no one ever forgets. + parentOpSet.getGTalkActiveCallsRepository().addCall(this); + } + + /** + * Determines if this call contains a peer whose corresponding session has + * the specified <tt>sid</tt>. + * + * @param sid the ID of the session whose peer we are looking for. + * + * @return <tt>true</tt> if this call contains a peer with the specified + * Google Talk <tt>sid</tt> and false otherwise. + */ + public boolean containsSessionID(String sid) + { + return (getPeer(sid) != null); + } + + /** + * Returns the peer whose corresponding session has the specified + * <tt>sid</tt>. + * + * @param sid the ID of the session whose peer we are looking for. + * + * @return the {@link CallPeerGTalkImpl} with the specified Google Talk + * <tt>sid</tt> and <tt>null</tt> if no such peer exists in this call. + */ + public CallPeerGTalkImpl getPeer(String sid) + { + for(CallPeerGTalkImpl peer : getCallPeersVector()) + { + if (peer.getSessionID().equals(sid)) + return peer; + } + return null; + } + + /** + * Returns the peer whose corresponding session-init ID has the specified + * <tt>id</tt>. + * + * @param id the ID of the session-init IQ whose peer we are looking for. + * + * @return the {@link CallPeerGTalkImpl} with the specified IQ + * <tt>id</tt> and <tt>null</tt> if no such peer exists in this call. + */ + public CallPeerGTalkImpl getPeerBySessInitPacketID(String id) + { + for(CallPeerGTalkImpl peer : getCallPeersVector()) + { + if (peer.getSessInitID().equals(id)) + return peer; + } + return null; + } + + /** + * Creates a new Google Talk call peer and sends a RINGING response. + * + * @param sessionIQ the {@link SessionIQ} that created the session. + * + * @return the newly created {@link CallPeerGTalkImpl} (the one that sent + * the INVITE). + */ + public CallPeerGTalkImpl processGTalkInitiate(SessionIQ sessionIQ) + { + String remoteParty = sessionIQ.getInitiator(); + + //according to the Jingle spec initiator may be null. + if (remoteParty == null) + remoteParty = sessionIQ.getFrom(); + + CallPeerGTalkImpl callPeer = new CallPeerGTalkImpl(remoteParty, this); + + addCallPeer(callPeer); + + //before notifying about this call, make sure that it looks alright + callPeer.processSessionInitiate(sessionIQ); + + if( callPeer.getState() == CallPeerState.FAILED) + return null; + + callPeer.setState( CallPeerState.INCOMING_CALL ); + + // if this was the first peer we added in this call then the call is + // new and we also need to notify everyone of its creation. + if(this.getCallPeerCount() == 1) + parentOpSet.fireCallEvent( CallEvent.CALL_RECEIVED, this); + + return callPeer; + } + + /** + * Creates a <tt>CallPeerGTalkImpl</tt> from <tt>calleeJID</tt> and sends + * them <tt>initiate</tt> IQ request. + * + * @param calleeJID the party that we would like to invite to this call. + * @param sessionInitiateExtensions a collection of additional and optional + * <tt>PacketExtension</tt>s to be added to the <tt>initiate</tt> + * {@link SessionIQ} which is to init this <tt>CallJabberImpl</tt> + * + * @return the newly created <tt>Call</tt> corresponding to + * <tt>calleeJID</tt>. 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 CallPeerGTalkImpl initiateGTalkSession( + String calleeJID, + Iterable<PacketExtension> sessionInitiateExtensions) + throws OperationFailedException + { + // create the session-initiate IQ + CallPeerGTalkImpl callPeer = new CallPeerGTalkImpl(calleeJID, this); + + 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 also need to notify everyone of its creation. + if(getCallPeerCount() == 1) + parentOpSet.fireCallEvent(CallEvent.CALL_INITIATED, this); + + CallPeerMediaHandlerGTalkImpl mediaHandler + = callPeer.getMediaHandler(); + + /* enable video if it is a videocall */ + mediaHandler.setLocalVideoTransmissionEnabled(localVideoAllowed); + + //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); + + callPeer.initiateSession(sessionInitiateExtensions); + + return callPeer; + } +} diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerGTalkImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerGTalkImpl.java new file mode 100644 index 0000000..41083b3 --- /dev/null +++ b/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerGTalkImpl.java @@ -0,0 +1,630 @@ +/* + * 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.reflect.*; +import java.util.*; + +import org.jivesoftware.smack.packet.*; + +import net.java.sip.communicator.impl.protocol.jabber.extensions.gtalk.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; +import net.java.sip.communicator.service.protocol.*; +import net.java.sip.communicator.service.protocol.event.*; +import net.java.sip.communicator.service.protocol.media.*; +import net.java.sip.communicator.util.*; + +/** + * Implements a Google Talk <tt>CallPeer</tt>. + * + * @author Sebastien Vincent + */ +public class CallPeerGTalkImpl + extends MediaAwareCallPeer<CallGTalkImpl, + CallPeerMediaHandlerGTalkImpl, + ProtocolProviderServiceJabberImpl> +{ + /** + * The <tt>Logger</tt> used by the <tt>CallPeerGTalkImpl</tt> class and its + * instances for logging output. + */ + private static final Logger logger + = Logger.getLogger(CallPeerGTalkImpl.class); + + /** + * The {@link SessionIQ} that created the session that this call represents. + */ + private SessionIQ sessionInitIQ = null; + + /** + * The jabber address of this peer + */ + private String peerJID = null; + + /** + * Indicates whether this peer was the one that initiated the session. + */ + protected boolean isInitiator = false; + + /** + * Session ID. + */ + private String sid = null; + + /** + * Creates a new call peer with address <tt>peerAddress</tt>. + * + * @param peerAddress the Google Talk address of the new call peer. + * @param owningCall the call that contains this call peer. + */ + public CallPeerGTalkImpl(String peerAddress, CallGTalkImpl owningCall) + { + super(owningCall); + this.peerJID = peerAddress; + setMediaHandler( new CallPeerMediaHandlerGTalkImpl(this) ); + } + + /** + * Returns a String locator for that peer. + * + * @return the peer's address or phone number. + */ + public String getAddress() + { + return peerJID; + } + + /** + * Specifies the address, phone number, or other protocol specific + * identifier that represents this call peer. This method is to be + * used by service users and MUST NOT be called by the implementation. + * + * @param address The address of this call peer. + */ + public void setAddress(String address) + { + String oldAddress = getAddress(); + + if(peerJID.equals(address)) + return; + + this.peerJID = address; + //Fire the Event + fireCallPeerChangeEvent( + CallPeerChangeEvent.CALL_PEER_ADDRESS_CHANGE, + oldAddress, + address); + } + + /** + * Returns a human readable name representing this peer. + * + * @return a String containing a name for that peer. + */ + public String getDisplayName() + { + if (getCall() != null) + { + Contact contact = getContact(); + + if (contact != null) + return contact.getDisplayName(); + } + return peerJID; + } + + /** + * Determines whether this peer was the one that initiated the session. Note + * that if this peer is the initiator of the session then this means we are + * the responder! + * + * @return <tt>true</tt> if this peer is the one that initiated the session + * and <tt>false</tt> otherwise (i.e. if _we_ initiated the session). + */ + public boolean isInitiator() + { + return isInitiator; + } + + /** + * Returns the contact corresponding to this peer or null if no + * particular contact has been associated. + * <p> + * @return the <tt>Contact</tt> corresponding to this peer or null + * if no particular contact has been associated. + */ + public Contact getContact() + { + ProtocolProviderService pps = getCall().getProtocolProvider(); + OperationSetPresence opSetPresence + = pps.getOperationSet(OperationSetPresence.class); + + return opSetPresence.findContactByID(getAddress()); + } + + /** + * Processes the session initiation {@link SessionIQ} that we were created + * with, passing its content to the media handler and then sends either a + * "session-info/ringing" or a "terminate" response. + * + * @param sessionInitIQ The {@link SessionIQ} that created the session that + * we are handling here. + */ + protected synchronized void processSessionInitiate(SessionIQ sessionInitIQ) + { + // Do initiate the session. + this.sessionInitIQ = sessionInitIQ; + this.isInitiator = true; + + RtpDescriptionPacketExtension description = null; + + for(PacketExtension ext : sessionInitIQ.getExtensions()) + { + if(ext.getElementName().equals( + RtpDescriptionPacketExtension.ELEMENT_NAME)) + { + description = (RtpDescriptionPacketExtension)ext; + break; + } + } + + if(description == null) + { + logger.info("No description in incoming session initiate"); + + //send an error response; + String reasonText = "Error: no description"; + SessionIQ errResp + = GTalkPacketFactory.createSessionTerminate( + sessionInitIQ.getTo(), + sessionInitIQ.getFrom(), + sessionInitIQ.getID(), + Reason.INCOMPATIBLE_PARAMETERS, + reasonText); + + setState(CallPeerState.FAILED, reasonText); + getProtocolProvider().getConnection().sendPacket(errResp); + return; + } + + try + { + getMediaHandler().processOffer(description); + } + catch(Exception ex) + { + logger.info("Failed to process an incoming session initiate", ex); + + //send an error response; + String reasonText = "Error: " + ex.getMessage(); + SessionIQ errResp + = GTalkPacketFactory.createSessionTerminate( + sessionInitIQ.getTo(), + sessionInitIQ.getFrom(), + sessionInitIQ.getID(), + Reason.INCOMPATIBLE_PARAMETERS, + reasonText); + + setState(CallPeerState.FAILED, reasonText); + getProtocolProvider().getConnection().sendPacket(errResp); + return; + } + } + + /** + * Initiate a Google Talk session {@link SessionIQ}. + * + * @param sessionInitiateExtensions a collection of additional and optional + * <tt>PacketExtension</tt>s to be added to the <tt>initiate</tt> + * {@link SessionIQ} which is to initiate the session with this + * <tt>CallPeerGTalkImpl</tt> + * @throws OperationFailedException exception + */ + protected synchronized void initiateSession( + Iterable<PacketExtension> sessionInitiateExtensions) + throws OperationFailedException + { + sid = SessionIQ.generateSID(); + isInitiator = false; + + //Create the media description that we'd like to send to the other side. + RtpDescriptionPacketExtension offer + = getMediaHandler().createDescription(); + + ProtocolProviderServiceJabberImpl protocolProvider + = getProtocolProvider(); + + sessionInitIQ + = GTalkPacketFactory.createSessionInitiate( + protocolProvider.getOurJID(), + this.peerJID, + sid, + offer); + + if (sessionInitiateExtensions != null) + { + for (PacketExtension sessionInitiateExtension + : sessionInitiateExtensions) + { + sessionInitIQ.addExtension(sessionInitiateExtension); + } + } + + protocolProvider.getConnection().sendPacket(sessionInitIQ); + + getMediaHandler().harvestCandidates(offer.getPayloadTypes(), + new CandidatesSender() + { + public void sendCandidates( + Iterable<GTalkCandidatePacketExtension> candidates) + { + CallPeerGTalkImpl.this.sendCandidates(candidates); + } + }); + } + + /** + * Puts this peer into a {@link CallPeerState#DISCONNECTED}, indicating a + * reason to the user, if there is one. + * + * @param sessionIQ the {@link SessionIQ} that's terminating our session. + */ + public void processSessionReject(SessionIQ sessionIQ) + { + processSessionTerminate(sessionIQ); + } + + /** + * Puts this peer into a {@link CallPeerState#DISCONNECTED}, indicating a + * reason to the user, if there is one. + * + * @param sessionIQ the {@link SessionIQ} that's terminating our session. + */ + public void processSessionTerminate(SessionIQ sessionIQ) + { + String reasonStr = "Call ended by remote side."; + ReasonPacketExtension reasonExt = sessionIQ.getReason(); + + if(reasonStr != null && reasonExt != null) + { + Reason reason = reasonExt.getReason(); + + if(reason != null) + reasonStr += " Reason: " + reason.toString() + "."; + + String text = reasonExt.getText(); + + if(text != null) + reasonStr += " " + text; + } + + getMediaHandler().getTransportManager().close(); + setState(CallPeerState.DISCONNECTED, reasonStr); + } + + /** + * Processes the session initiation {@link SessionIQ} that we were created + * with, passing its content to the media handler. + * + * @param sessionInitIQ The {@link SessionIQ} that created the session that + * we are handling here. + */ + public void processSessionAccept(SessionIQ sessionInitIQ) + { + this.sessionInitIQ = sessionInitIQ; + + CallPeerMediaHandlerGTalkImpl mediaHandler = getMediaHandler(); + Collection<PacketExtension> extensions = + sessionInitIQ.getExtensions(); + RtpDescriptionPacketExtension answer = null; + + for(PacketExtension ext : extensions) + { + if(ext.getElementName().equalsIgnoreCase( + RtpDescriptionPacketExtension.ELEMENT_NAME)) + { + answer = (RtpDescriptionPacketExtension)ext; + break; + } + } + + try + { + mediaHandler.getTransportManager(). + wrapupConnectivityEstablishment(); + mediaHandler.processAnswer(answer); + } + catch(Exception exc) + { + if (logger.isInfoEnabled()) + logger.info("Failed to process a session-accept", exc); + + //send an error response + String reasonText = "Error: " + exc.getMessage(); + SessionIQ errResp + = GTalkPacketFactory.createSessionTerminate( + sessionInitIQ.getTo(), + sessionInitIQ.getFrom(), + sessionInitIQ.getID(), + Reason.GENERAL_ERROR, + reasonText); + + getMediaHandler().getTransportManager().close(); + setState(CallPeerState.FAILED, reasonText); + getProtocolProvider().getConnection().sendPacket(errResp); + return; + } + + //tell everyone we are connecting so that the audio notifications would + //stop + setState(CallPeerState.CONNECTED); + + mediaHandler.start(); + } + + /** + * Process candidates received. + * + * @param sessionInitIQ The {@link SessionIQ} that created the session we + * are handling here + */ + public void processCandidates(SessionIQ sessionInitIQ) + { + Collection<PacketExtension> extensions = sessionInitIQ.getExtensions(); + List<GTalkCandidatePacketExtension> candidates = + new ArrayList<GTalkCandidatePacketExtension>(); + + for(PacketExtension ext : extensions) + { + if(ext.getElementName().equalsIgnoreCase( + GTalkCandidatePacketExtension.ELEMENT_NAME)) + { + GTalkCandidatePacketExtension cand = + (GTalkCandidatePacketExtension)ext; + candidates.add(cand); + } + } + + try + { + getMediaHandler().processCandidates(candidates); + } + catch (OperationFailedException ofe) + { + logger.warn("Failed to process an incoming candidates", ofe); + + //send an error response + String reasonText = "Error: " + ofe.getMessage(); + SessionIQ errResp + = GTalkPacketFactory.createSessionTerminate( + sessionInitIQ.getTo(), + sessionInitIQ.getFrom(), + sessionInitIQ.getID(), + Reason.GENERAL_ERROR, + reasonText); + + getMediaHandler().getTransportManager().close(); + setState(CallPeerState.FAILED, reasonText); + getProtocolProvider().getConnection().sendPacket(errResp); + return; + } + } + + /** + * Returns the session ID of the Jingle session associated with this call. + * + * @return the session ID of the Jingle session associated with this call. + */ + public String getSessionID() + { + return sessionInitIQ != null ? sessionInitIQ.getID() : sid; + } + + /** + * Returns the IQ ID of the Jingle session-initiate packet associated with + * this call. + * + * @return the IQ ID of the Jingle session-initiate packet associated with + * this call. + */ + public String getSessInitID() + { + return sessionInitIQ != null ? sessionInitIQ.getPacketID() : null; + } + + /** + * Ends the call with for this <tt>CallPeer</tt>. Depending on the state + * of the peer the method would send a CANCEL, BYE, or BUSY_HERE message + * and set the new state to DISCONNECTED. + * + * @param reasonText the text, if any, to be set on the + * <tt>ReasonPacketExtension</tt> as the value of its + * @param reasonOtherExtension the <tt>PacketExtension</tt>, if any, to be + * set on the <tt>ReasonPacketExtension</tt> as the value of its + * <tt>otherExtension</tt> property + */ + public void hangup(String reasonText, PacketExtension reasonOtherExtension) + { + // do nothing if the call is already ended + if (CallPeerState.DISCONNECTED.equals(getState()) + || CallPeerState.FAILED.equals(getState())) + { + if (logger.isDebugEnabled()) + logger.debug("Ignoring a request to hangup a call peer " + + "that is already DISCONNECTED"); + return; + } + + CallPeerState prevPeerState = getState(); + getMediaHandler().getTransportManager().close(); + setState(CallPeerState.DISCONNECTED); + SessionIQ responseIQ = null; + + if (prevPeerState.equals(CallPeerState.CONNECTED) + || CallPeerState.isOnHold(prevPeerState)) + { + responseIQ = GTalkPacketFactory.createBye( + getProtocolProvider().getOurJID(), peerJID, getSessionID()); + } + else if (CallPeerState.CONNECTING.equals(prevPeerState) + || CallPeerState.CONNECTING_WITH_EARLY_MEDIA.equals(prevPeerState) + || CallPeerState.ALERTING_REMOTE_SIDE.equals(prevPeerState)) + { + responseIQ = GTalkPacketFactory.createCancel( + getProtocolProvider().getOurJID(), peerJID, getSessionID()); + } + else if (prevPeerState.equals(CallPeerState.INCOMING_CALL)) + { + responseIQ = GTalkPacketFactory.createBusy( + getProtocolProvider().getOurJID(), peerJID, getSessionID()); + } + else if (prevPeerState.equals(CallPeerState.BUSY) + || prevPeerState.equals(CallPeerState.FAILED)) + { + // For FAILED and BUSY we only need to update CALL_STATUS + // as everything else has been done already. + } + else + { + logger.info("Could not determine call peer state!"); + } + + if (responseIQ != null) + { + if (reasonOtherExtension != null) + { + ReasonPacketExtension reason + = (ReasonPacketExtension) + responseIQ.getExtension( + ReasonPacketExtension.ELEMENT_NAME, + ReasonPacketExtension.NAMESPACE); + + if (reason != null) + reason.setOtherExtension(reasonOtherExtension); + } + + getProtocolProvider().getConnection().sendPacket(responseIQ); + } + } + + /** + * Indicates a user request to answer an incoming call from this + * <tt>CallPeer</tt>. + * + * Sends an OK response to <tt>callPeer</tt>. Make sure that the call + * peer contains an SDP description when you call this method. + * + * @throws OperationFailedException if we fail to create or send the + * response. + */ + public synchronized void answer() + throws OperationFailedException + { + System.out.println("answer"); + RtpDescriptionPacketExtension answer = null; + + try + { + getMediaHandler().getTransportManager(). + wrapupConnectivityEstablishment(); + answer = getMediaHandler().generateSessionAccept(); + } + catch(Exception exc) + { + logger.info("Failed to answer an incoming call", exc); + + //send an error response + String reasonText = "Error: " + exc.getMessage(); + SessionIQ errResp + = GTalkPacketFactory.createSessionTerminate( + sessionInitIQ.getTo(), + sessionInitIQ.getFrom(), + sessionInitIQ.getID(), + Reason.FAILED_APPLICATION, + reasonText); + + setState(CallPeerState.FAILED, reasonText); + getProtocolProvider().getConnection().sendPacket(errResp); + return; + } + + SessionIQ response + = GTalkPacketFactory.createSessionAccept( + sessionInitIQ.getTo(), + sessionInitIQ.getFrom(), + getSessionID(), + answer); + + //send the packet first and start the stream later in case the media + //relay needs to see it before letting hole punching techniques through. + getProtocolProvider().getConnection().sendPacket(response); + + try + { + getMediaHandler().start(); + } + catch(UndeclaredThrowableException e) + { + Throwable exc = e.getUndeclaredThrowable(); + + logger.info("Failed to establish a connection", exc); + + //send an error response + String reasonText = "Error: " + exc.getMessage(); + SessionIQ errResp + = GTalkPacketFactory.createSessionTerminate( + sessionInitIQ.getTo(), + sessionInitIQ.getFrom(), + sessionInitIQ.getID(), + Reason.GENERAL_ERROR, + reasonText); + + getMediaHandler().getTransportManager().close(); + setState(CallPeerState.FAILED, reasonText); + getProtocolProvider().getConnection().sendPacket(errResp); + return; + } + + //tell everyone we are connecting so that the audio notifications would + //stop + setState(CallPeerState.CONNECTED); + } + + /** + * Sends local candidate addresses from the local peer to the remote peer + * using the <tt>candidates</tt> {@link SessionIQ}. + * + * @param candidates the local candidate addresses to be sent from the local + * peer to the remote peer using the <tt>candidates</tt> + * {@link SessionIQ} + */ + protected void sendCandidates( + Iterable<GTalkCandidatePacketExtension> candidates) + { + + ProtocolProviderServiceJabberImpl protocolProvider + = getProtocolProvider(); + + SessionIQ candidatesIQ = new SessionIQ(); + + candidatesIQ.setGTalkType(GTalkType.CANDIDATES); + candidatesIQ.setFrom(protocolProvider.getOurJID()); + candidatesIQ.setInitiator(isInitiator() ? getAddress() : + protocolProvider.getOurJID()); + candidatesIQ.setID(getSessionID()); + candidatesIQ.setTo(getAddress()); + candidatesIQ.setType(IQ.Type.SET); + + for (GTalkCandidatePacketExtension candidate : candidates) + { + candidatesIQ.addExtension(candidate); + } + System.out.println("IQ: " + candidatesIQ.toXML()); + + protocolProvider.getConnection().sendPacket(candidatesIQ); + } +} diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerMediaHandlerGTalkImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerMediaHandlerGTalkImpl.java new file mode 100644 index 0000000..46885db --- /dev/null +++ b/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerMediaHandlerGTalkImpl.java @@ -0,0 +1,685 @@ +/* + * 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.reflect.*; +import java.util.*; + +import net.java.sip.communicator.impl.protocol.jabber.extensions.gtalk.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; +import net.java.sip.communicator.impl.protocol.jabber.jinglesdp.*; +import net.java.sip.communicator.service.neomedia.*; +import net.java.sip.communicator.service.neomedia.device.*; +import net.java.sip.communicator.service.neomedia.format.*; +import net.java.sip.communicator.service.protocol.*; +import net.java.sip.communicator.service.protocol.media.*; +import net.java.sip.communicator.util.*; + +/** + * An Google Talk specific extension of the generic media handler. + * + * @author Sebastien Vincent + */ +public class CallPeerMediaHandlerGTalkImpl + extends CallPeerMediaHandler<CallPeerGTalkImpl> +{ + /** + * The <tt>Logger</tt> used by the <tt>CallPeerMediaHandlerGTalkImpl</tt> + * class and its instances for logging output. + */ + private static final Logger logger + = Logger.getLogger(CallPeerMediaHandlerGTalkImpl.class); + + /** + * Google Talk name for the audio part. + */ + public static final String AUDIO_RTP = "rtp"; + + /** + * Google Talk name for the video part. + */ + public static final String VIDEO_RTP = "video_rtp"; + + /** + * The current description of the streams that we have going toward the + * remote side. We use {@link LinkedHashMap}s to make sure that we preserve + * the order of the individual content extensions. + */ + private Map<String, List<PayloadTypePacketExtension>> localContentMap + = new LinkedHashMap<String, List<PayloadTypePacketExtension> >(); + + /** + * The current description of the streams that the remote side has with us. + * We use {@link LinkedHashMap}s to make sure that we preserve + * the order of the individual content extensions. + */ + private Map<String, List<PayloadTypePacketExtension> > remoteContentMap + = new LinkedHashMap<String, List<PayloadTypePacketExtension> >(); + + /** + * The <tt>TransportManager</tt> implementation handling our address + * management. + */ + private TransportManagerGTalkImpl transportManager; + + /** + * Creates a new handler that will be managing media streams for + * <tt>peer</tt>. + * + * @param peer that <tt>CallPeerGTalkImpl</tt> instance that we will be + * managing media for. + */ + public CallPeerMediaHandlerGTalkImpl(CallPeerGTalkImpl peer) + { + super(peer, peer); + } + + /** + * Lets the underlying implementation take note of this error and only + * then throws it to the using bundles. + * + * @param message the message to be logged and then wrapped in a new + * <tt>OperationFailedException</tt> + * @param errorCode the error code to be assigned to the new + * <tt>OperationFailedException</tt> + * @param cause the <tt>Throwable</tt> that has caused the necessity to log + * an error and have a new <tt>OperationFailedException</tt> thrown + * + * @throws OperationFailedException the exception that we wanted this method + * to throw. + */ + protected void throwOperationFailedException( + String message, + int errorCode, + Throwable cause) + throws OperationFailedException + { + ProtocolProviderServiceJabberImpl.throwOperationFailedException( + message, + errorCode, + cause, + logger); + } + + /** + * Parses and handles the specified <tt>offer</tt> and returns a content + * extension representing the current state of this media handler. This + * method MUST only be called when <tt>offer</tt> is the first session + * description that this <tt>MediaHandler</tt> is seeing. + * + * @param offer the offer that we'd like to parse, handle and get an answer + * for. + * + * @throws OperationFailedException if we have a problem satisfying the + * description received in <tt>offer</tt> (e.g. failed to open a device or + * initialize a stream ...). + * @throws IllegalArgumentException if there's a problem with + * <tt>offer</tt>'s format or semantics. + */ + public void processOffer(RtpDescriptionPacketExtension offer) + throws OperationFailedException, + IllegalArgumentException + { + List<PayloadTypePacketExtension> payloadTypes = offer.getPayloadTypes(); + boolean atLeastOneValidDescription = false; + List<PayloadTypePacketExtension> answer = + new ArrayList<PayloadTypePacketExtension>(); + List<MediaFormat> remoteFormats = JingleUtils.extractFormats( + offer, + getDynamicPayloadTypes()); + boolean isAudio = false; + boolean isVideo = false; + + for(PayloadTypePacketExtension ext : payloadTypes) + { + if(ext.getNamespace().equals( + SessionIQProvider.GTALK_AUDIO_NAMESPACE)) + { + isAudio = true; + } + else if(ext.getNamespace().equals( + SessionIQProvider.GTALK_VIDEO_NAMESPACE)) + { + isVideo = true; + } + } + + for(MediaType mediaType : MediaType.values()) + { + if(!(isAudio && mediaType == MediaType.AUDIO) && + !(isVideo && mediaType == MediaType.VIDEO)) + { + continue; + } + + remoteContentMap.put(mediaType.toString(), payloadTypes); + + MediaDevice dev = getDefaultDevice(mediaType); + + MediaDirection devDirection + = (dev == null) ? MediaDirection.INACTIVE : dev.getDirection(); + + // Take the preference of the user with respect to streaming + // mediaType into account. + devDirection + = devDirection.and(getDirectionUserPreference(mediaType)); + + // intersect the MediaFormats of our device with remote ones + List<MediaFormat> mutuallySupportedFormats + = intersectFormats(remoteFormats, dev.getSupportedFormats()); + + List<PayloadTypePacketExtension> contents + = createPayloadTypesForOffer( + getNameForMediaType(mediaType), + mutuallySupportedFormats); + answer.addAll(contents); + + localContentMap.put(mediaType.toString(), answer); + + atLeastOneValidDescription = true; + } + + + if (!atLeastOneValidDescription) + { + ProtocolProviderServiceJabberImpl.throwOperationFailedException( + "Offer contained no media formats" + + " or no valid media descriptions.", + OperationFailedException.ILLEGAL_ARGUMENT, + null, + logger); + } + + /* + * In order to minimize post-pickup delay, start establishing the + * connectivity prior to ringing. + */ + harvestCandidates( + answer, + new CandidatesSender() + { + public void sendCandidates( + Iterable<GTalkCandidatePacketExtension> candidates) + { + getPeer().sendCandidates(candidates); + } + }); + } + + /** + * Wraps up any ongoing candidate harvests and returns our response to the + * last offer we've received, so that the peer could use it to send a + * <tt>accept</tt>. + * + * @return the last generated list of + * {@link RtpDescriptionPacketExtension}s that the call peer could use to + * send a <tt>accept</tt>. + * + * @throws OperationFailedException if we fail to configure the media stream + */ + public RtpDescriptionPacketExtension generateSessionAccept() + throws OperationFailedException + { + RtpDescriptionPacketExtension description = + new RtpDescriptionPacketExtension(); + List<PayloadTypePacketExtension> lst = localContentMap.get("audio"); + + description.setNamespace(SessionIQProvider.GTALK_AUDIO_NAMESPACE); + + for(MediaType mediaType : MediaType.values()) + { + MediaFormat format = null; + String ns = getNamespaceForMediaType(mediaType); + String mediaName = getNameForMediaType(mediaType); + + for(PayloadTypePacketExtension ext : lst) + { + if(ext.getNamespace().equals(ns)) + { + format = JingleUtils.payloadTypeToMediaFormat( + ext, + getDynamicPayloadTypes()); + description.addPayloadType(ext); + + if(format == null) + { + continue; + } + + break; + } + } + + if(format == null) + { + continue; + } + + System.out.println("session format " + format); + + // stream connector + StreamConnector connector + = transportManager.getStreamConnector(mediaType); + + //the device this stream would be reading from and writing to. + MediaDevice dev = getDefaultDevice(mediaType); + + // stream target + MediaStreamTarget target = transportManager.getStreamTarget( + mediaType); + + List<RTPExtension> rtpExtensions = + new ArrayList<RTPExtension>(); + MediaDirection direction = MediaDirection.SENDRECV; + + initStream(mediaName, connector, dev, format, target, + direction, rtpExtensions); + + if(mediaType == MediaType.VIDEO) + { + description.setNamespace( + SessionIQProvider.GTALK_VIDEO_NAMESPACE); + } + } + + return description; + } + + /** + * Handles the specified <tt>answer</tt> by creating and initializing the + * corresponding <tt>MediaStream</tt>s. + * + * @param answer the Google Talk answer + * + * @throws OperationFailedException if we fail to handle <tt>answer</tt> for + * reasons like failing to initialize media devices or streams. + * @throws IllegalArgumentException if there's a problem with the syntax or + * the semantics of <tt>answer</tt>. Method is synchronized in order to + * avoid closing mediaHandler when we are currently in process of + * initializing, configuring and starting streams and anybody interested + * in this operation can synchronize to the mediaHandler instance to wait + * processing to stop (method setState in CallPeer). + */ + public void processAnswer(RtpDescriptionPacketExtension answer) + throws OperationFailedException, + IllegalArgumentException + { + List<PayloadTypePacketExtension> lst = answer.getPayloadTypes(); + + for(MediaType mediaType : MediaType.values()) + { + String ns = getNamespaceForMediaType(mediaType); + String mediaName = getNameForMediaType(mediaType); + MediaFormat format = null; + + for(PayloadTypePacketExtension ext : lst) + { + if(ext.getNamespace().equals(ns)) + { + format = JingleUtils.payloadTypeToMediaFormat( + ext, + getDynamicPayloadTypes()); + + if(format == null) + { + continue; + } + break; + } + } + + if(format == null) + { + continue; + } + + System.out.println("format: " + format); + + // stream connector + StreamConnector connector + = transportManager.getStreamConnector(mediaType); + + //the device this stream would be reading from and writing to. + MediaDevice dev = getDefaultDevice(mediaType); + + // stream target + MediaStreamTarget target = transportManager.getStreamTarget( + mediaType); + + List<RTPExtension> rtpExtensions = new ArrayList<RTPExtension>(); + MediaDirection direction = MediaDirection.SENDRECV; + + initStream(mediaName, connector, dev, format, target, + direction, rtpExtensions); + } + } + + /** + * Gets the <tt>TransportManager</tt> implementation handling our address + * management. + * + * @return the <tt>TransportManager</tt> implementation handling our address + * management + * @see CallPeerMediaHandler#getTransportManager() + */ + protected TransportManagerGTalkImpl getTransportManager() + { + if (transportManager == null) + { + /* Google Talk assumes to use ICE */ + CallPeerGTalkImpl peer = getPeer(); + + // support for Google Talk + transportManager = new TransportManagerGTalkImpl(peer); + } + return transportManager; + } + + /** + * Processes the transport-related information provided by the remote + * <tt>peer</tt> in a specific set of <tt>CandidatePacketExtension</tt>s. + * + * @param candidates the <tt>CandidatePacketExtenion</tt>s provided by the + * remote <tt>peer</tt> and containing the candidate-related information to + * be processed + * @throws OperationFailedException if anything goes wrong while processing + * the candidate-related information provided by the remote <tt>peer</tt> in + * the specified set of <tt>CandidatePacketExtension</tt>s + */ + public void processCandidates( + Iterable<GTalkCandidatePacketExtension> candidates) + throws OperationFailedException + { + getTransportManager().startConnectivityEstablishment(candidates); + } + + /** + * Creates a <tt>List</tt> containing the {@link ContentPacketExtension}s of + * the streams that this handler is prepared to initiate depending on + * available <tt>MediaDevice</tt>s and local on-hold and video transmission + * preferences. + * + * @return a <tt>RtpDescriptionPacketExtension</tt> that contains + * list of <tt>PayloadTypePacketExtension</tt> + * + * @throws OperationFailedException if we fail to create the descriptions + * for reasons like problems with device interaction, allocating ports, etc. + */ + public RtpDescriptionPacketExtension createDescription() + throws OperationFailedException + { + RtpDescriptionPacketExtension description = + new RtpDescriptionPacketExtension( + SessionIQProvider.GTALK_AUDIO_NAMESPACE); + List<PayloadTypePacketExtension> mediaDescs + = new ArrayList<PayloadTypePacketExtension>(); + boolean isVideo = false; + + for (MediaType mediaType : MediaType.values()) + { + MediaDevice dev = getDefaultDevice(mediaType); + + if (dev != null) + { + MediaDirection direction = dev.getDirection().and( + getDirectionUserPreference(mediaType)); + + if(isLocallyOnHold()) + direction = direction.and(MediaDirection.SENDONLY); + + /* + * If we're only able to receive, we don't have to offer it at + * all. For example, we have to offer audio and no video when we + * start an audio call. + */ + if (MediaDirection.RECVONLY.equals(direction)) + direction = MediaDirection.INACTIVE; + + if(direction != MediaDirection.INACTIVE) + { + List<PayloadTypePacketExtension> contents + = createPayloadTypesForOffer( + getNameForMediaType(mediaType), + dev.getSupportedFormats()); + + for(PayloadTypePacketExtension ext : contents) + { + /* if we add one "video" payload type, we must + * advertise it in the description IQ + */ + if(!isVideo && mediaType.equals(MediaType.VIDEO)) + { + description.setNamespace(SessionIQProvider. + GTALK_VIDEO_NAMESPACE); + + isVideo = true; + } + description.addChildExtension(ext); + mediaDescs.add(ext); + } + } + } + } + + //fail if all devices were inactive + if(mediaDescs.isEmpty()) + { + ProtocolProviderServiceJabberImpl.throwOperationFailedException( + "We couldn't find any active Audio/Video devices" + + " and couldn't create a call", + OperationFailedException.GENERAL_ERROR, + null, + logger); + } + + return description; + } + + /** + * Gathers local candidate addresses. + * + * @param local the media descriptions sent or to be sent from the local + * peer to the remote peer. + * @param candidatesSender the <tt>CandidatesSender</tt> to be used by + * this <tt>TransportManagerGTalkImpl</tt> to send <tt>candidates</tt> + * <tt>SessionIQ</tt>s from the local peer to the remote peer if this + * <tt>TransportManagerGTalkImpl</tt> wishes to utilize <tt>candidates</tt> + * @throws OperationFailedException if anything goes wrong while starting or + * wrapping up the gathering of local candidate addresses + */ + protected void harvestCandidates( + List<PayloadTypePacketExtension> local, + CandidatesSender candidatesSender) + throws OperationFailedException + { + getTransportManager().startCandidateHarvest( + local, + candidatesSender); + + transportManager.wrapupCandidateHarvest(); + } + + /** + * Get Google Talk name for the media type. + * + * @param mediaType media type + * @return name for the media type + * @throws IllegalArgumentException if media type is not audio or video + */ + private static String getNameForMediaType(MediaType mediaType) + throws IllegalArgumentException + { + if(mediaType == MediaType.AUDIO) + { + return AUDIO_RTP; + } + else if(mediaType == MediaType.VIDEO) + { + return VIDEO_RTP; + } + else + { + throw new IllegalArgumentException("not a mediatype"); + } + } + + /** + * Get Google Talk namespace for the media type. + * + * @param mediaType media type + * @return namespace for the media type + * @throws IllegalArgumentException if media type is not audio or video + */ + private static String getNamespaceForMediaType(MediaType mediaType) + { + + if(mediaType == MediaType.AUDIO) + { + return SessionIQProvider.GTALK_AUDIO_NAMESPACE; + } + else if(mediaType == MediaType.VIDEO) + { + return SessionIQProvider.GTALK_VIDEO_NAMESPACE; + } + else + { + throw new IllegalArgumentException("not a mediatype"); + } + } + + /** + * Create list of payload types for device. + * + * @param supportedFormats supported formats of a device + * @param direction direction + * @param supportedExtensions supported RTP extensions + * @return list of payload types for this device + */ + private List<PayloadTypePacketExtension> createPayloadTypesForOffer( + String name, + List<MediaFormat> supportedFormats) + { + List<PayloadTypePacketExtension> peList = + new ArrayList<PayloadTypePacketExtension>(); + + for(MediaFormat fmt : supportedFormats) + { + PayloadTypePacketExtension ext = + JingleUtils.formatToPayloadType(fmt, getDynamicPayloadTypes()); + ext.setNamespace(name.equals(AUDIO_RTP) ? + SessionIQProvider.GTALK_AUDIO_NAMESPACE : + SessionIQProvider.GTALK_VIDEO_NAMESPACE); + peList.add(ext); + } + + return peList; + } + + /** + * Waits for the associated <tt>TransportManagerJabberImpl</tt> to conclude + * any started connectivity establishment and then starts this + * <tt>CallPeerMediaHandler</tt>. + * + * @throws IllegalStateException if no offer or answer has been provided or + * generated earlier + */ + @Override + public void start() + throws IllegalStateException + { + try + { + wrapupConnectivityEstablishment(); + } + catch (OperationFailedException ofe) + { + throw new UndeclaredThrowableException(ofe); + } + super.start(); + } + + /** + * Notifies the associated <tt>TransportManagerGTalkImpl</tt> that it + * should conclude any connectivity establishment, waits for it to actually + * do so and sets the <tt>connector</tt>s and <tt>target</tt>s of the + * <tt>MediaStream</tt>s managed by this <tt>CallPeerMediaHandler</tt>. + * + * @throws OperationFailedException if anything goes wrong while setting the + * <tt>connector</tt>s and/or <tt>target</tt>s of the <tt>MediaStream</tt>s + * managed by this <tt>CallPeerMediaHandler</tt> + */ + private void wrapupConnectivityEstablishment() + throws OperationFailedException + { + TransportManagerGTalkImpl transportManager = getTransportManager(); + + transportManager.wrapupConnectivityEstablishment(); + + for (MediaType mediaType : MediaType.values()) + { + MediaStream stream = getStream(mediaType); + + if (stream != null) + { + stream.setConnector( + transportManager.getStreamConnector(mediaType)); + stream.setTarget(transportManager.getStreamTarget(mediaType)); + } + } + } + + /** + * Creates if necessary, and configures the stream that this + * <tt>MediaHandler</tt> is using for the <tt>MediaType</tt> matching the + * one of the <tt>MediaDevice</tt>. This method extends the one already + * available by adding a stream name, corresponding to a stream's content + * name. + * + * @param streamName the name of the stream as indicated in the XMPP + * <tt>content</tt> element. + * @param connector the <tt>MediaConnector</tt> that we'd like to bind the + * newly created stream to. + * @param device the <tt>MediaDevice</tt> that we'd like to attach the newly + * created <tt>MediaStream</tt> to. + * @param format the <tt>MediaFormat</tt> that we'd like the new + * <tt>MediaStream</tt> to be set to transmit in. + * @param target the <tt>MediaStreamTarget</tt> containing the RTP and RTCP + * address:port couples that the new stream would be sending packets to. + * @param direction the <tt>MediaDirection</tt> that we'd like the new + * stream to use (i.e. sendonly, sendrecv, recvonly, or inactive). + * @param rtpExtensions the list of <tt>RTPExtension</tt>s that should be + * enabled for this stream. + * + * @return the newly created <tt>MediaStream</tt>. + * + * @throws OperationFailedException if creating the stream fails for any + * reason (like for example accessing the device or setting the format). + */ + protected MediaStream initStream(String streamName, + StreamConnector connector, + MediaDevice device, + MediaFormat format, + MediaStreamTarget target, + MediaDirection direction, + List<RTPExtension> rtpExtensions) + throws OperationFailedException + { + MediaStream stream + = super.initStream( + connector, + device, + format, + target, + direction, + rtpExtensions); + + if(stream != null) + stream.setName(streamName); + + return stream; + } +} diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/CandidatesSender.java b/src/net/java/sip/communicator/impl/protocol/jabber/CandidatesSender.java new file mode 100644 index 0000000..1a87c84 --- /dev/null +++ b/src/net/java/sip/communicator/impl/protocol/jabber/CandidatesSender.java @@ -0,0 +1,31 @@ +/* + * 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 net.java.sip.communicator.impl.protocol.jabber.extensions.gtalk.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; + +/** + * Represents functionality which allows a <tt>TransportManagerGTalkImpl</tt> + * implementation to send <tt>candidates</tt> {@link SessionIQ}s for the + * purposes of expediting candidate negotiation. + * + * @author Sebastien Vincent + */ +public interface CandidatesSender +{ + /** + * Sends specific {@link CandidatePacketExtension}s in a <tt>candidates</tt> + * {@link SessionIQ} from the local peer to the remote peer. + * + * @param candidates the <tt>CandidatePacketExtension</tt>s to be sent in a + * <tt>candidates</tt> <tt>SessionIQ</tt> from the local peer to the + * remote peer + */ + public void sendCandidates( + Iterable<GTalkCandidatePacketExtension> candidates); +} diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/IceUdpTransportManager.java b/src/net/java/sip/communicator/impl/protocol/jabber/IceUdpTransportManager.java index de0b49f..7d84794 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/IceUdpTransportManager.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/IceUdpTransportManager.java @@ -491,7 +491,7 @@ public class IceUdpTransportManager throws OperationFailedException { Collection<ContentPacketExtension> transportInfoContents - = (transportInfoSender ==null) + = (transportInfoSender == null) ? null : new LinkedList<ContentPacketExtension>(); diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetAvatarJabberImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetAvatarJabberImpl.java index f7d695a..a495343 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetAvatarJabberImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetAvatarJabberImpl.java @@ -1,6 +1,6 @@ /* * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. - * + * * Distributable under LGPL license. See terms of license at gnu.org. */ package net.java.sip.communicator.impl.protocol.jabber; @@ -10,16 +10,24 @@ import net.java.sip.communicator.service.protocol.*; /** * A simple implementation of the <tt>OperationSetAvatar</tt> interface for the * jabber protocol. - * + * * Actually there isn't any maximum size for the jabber protocol but GoogleTalk * fix it a 96x96. - * + * * @author Damien Roth */ public class OperationSetAvatarJabberImpl extends AbstractOperationSetAvatar<ProtocolProviderServiceJabberImpl> { + /** + * Creates a new instances of <tt>OperationSetAvatarJabberImpl</tt>. + * + * @param parentProvider a reference to the + * <tt>ProtocolProviderServiceJabberImpl</tt> instance that created us. + * @param accountInfoOpSet a reference to the + * <tt>OperationSetServerStoredAccountInfo</tt>. + */ public OperationSetAvatarJabberImpl( ProtocolProviderServiceJabberImpl parentProvider, OperationSetServerStoredAccountInfo accountInfoOpSet) diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetBasicTelephonyJabberImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetBasicTelephonyJabberImpl.java index 2ce3529..a96f1e1 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetBasicTelephonyJabberImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetBasicTelephonyJabberImpl.java @@ -59,6 +59,13 @@ public class OperationSetBasicTelephonyJabberImpl = new ActiveCallsRepositoryJabberImpl(this); /** + * Contains references for all currently active (non ended) Google Talk + * calls. + */ + private ActiveCallsRepositoryGTalkImpl activeGTalkCallsRepository + = new ActiveCallsRepositoryGTalkImpl(this); + + /** * Creates a new instance. * * @param protocolProvider a reference to the @@ -343,6 +350,16 @@ public class OperationSetBasicTelephonyJabberImpl } /** + * Returns an iterator over all currently Google Talk active calls. + * + * @return an iterator over all currently Google Talk active calls. + */ + public Iterator<CallGTalkImpl> getGTalkActiveCalls() + { + return activeGTalkCallsRepository.getActiveCalls(); + } + + /** * Resumes communication with a call peer previously put on hold. * * @param peer the call peer to put on hold. @@ -776,6 +793,18 @@ public class OperationSetBasicTelephonyJabberImpl } /** + * Returns a reference to the {@link ActiveCallsRepositoryGTalkImpl} that + * we are currently using. + * + * @return a reference to the {@link ActiveCallsRepositoryGTalkImpl} that + * we are currently using. + */ + protected ActiveCallsRepositoryGTalkImpl getGTalkActiveCallsRepository() + { + return activeGTalkCallsRepository; + } + + /** * Returns the protocol provider that this operation set belongs to. * * @return a reference to the <tt>ProtocolProviderService</tt> that created diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetDesktopSharingServerJabberImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetDesktopSharingServerJabberImpl.java index 4d1c043..445b3b4 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetDesktopSharingServerJabberImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetDesktopSharingServerJabberImpl.java @@ -265,8 +265,6 @@ public class OperationSetDesktopSharingServerJabberImpl /* enable remote-control */ call.setLocalInputEvtAware(supported); - - basicTelephony.createOutgoingCall(call, calleeAddress); return call; } diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/TransportManagerGTalkImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/TransportManagerGTalkImpl.java new file mode 100644 index 0000000..b43d82b --- /dev/null +++ b/src/net/java/sip/communicator/impl/protocol/jabber/TransportManagerGTalkImpl.java @@ -0,0 +1,891 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.protocol.jabber; + +import java.beans.*; +import java.net.*; +import java.util.*; + +import org.ice4j.*; +import org.ice4j.ice.*; + +import net.java.sip.communicator.impl.protocol.jabber.extensions.gtalk.*; +import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; +import net.java.sip.communicator.service.neomedia.*; +import net.java.sip.communicator.service.netaddr.*; +import net.java.sip.communicator.service.protocol.*; +import net.java.sip.communicator.service.protocol.media.*; +import net.java.sip.communicator.util.*; + +/** + * <tt>TransportManager</tt>s gather local candidates for incoming and outgoing + * calls. Their work starts by calling a start method which, using the remote + * peer's session description, would start the harvest. Calling a second wrapup + * method would deliver the candidate harvest, possibly after blocking if it has + * not yet completed. + * + * @author Emil Ivov + * @author Lyubomir Marinov + * @author Sebastien Vincent + */ +public class TransportManagerGTalkImpl + extends TransportManager<CallPeerGTalkImpl> +{ + /** + * The <tt>Logger</tt> used by the <tt>IceUdpTransportManager</tt> + * class and its instances for logging output. + */ + private static final Logger logger + = Logger.getLogger(TransportManagerGTalkImpl.class); + + /** + * The generation of the candidates we are currently generating + */ + private int currentGeneration = 0; + + /** + * The ID that we will be assigning to our next candidate. We use + * <tt>int</tt>s for interoperability reasons (Emil: I believe that GTalk + * uses <tt>int</tt>s. If that turns out not to be the case we can stop + * using <tt>int</tt>s here if that's an issue). + */ + private static int nextID = 1; + + /** + * The ICE <tt>Component</tt> IDs in their common order used, for example, + * by <tt>DefaultStreamConnector</tt>, <tt>MediaStreamTarget</tt>. + */ + private static final int[] COMPONENT_IDS + = new int[] { Component.RTP, Component.RTCP }; + + /** + * The ICE agent that this transport manager would be using for ICE + * negotiation. + */ + private final Agent iceAgent; + + /** + * Creates a new instance of this transport manager, binding it to the + * specified peer. + * + * @param callPeer the {@link CallPeer} whose traffic we will be taking + * care of. + */ + public TransportManagerGTalkImpl(CallPeerGTalkImpl callPeer) + { + super(callPeer); + + iceAgent = createIceAgent(); + } + + /** + * Returns the ID that we will be assigning to the next candidate we create. + * + * @return the next ID to use with a candidate. + */ + protected String getNextID() + { + return Integer.toString(nextID++); + } + + /** + * Returns the generation that our current candidates belong to. + * + * @return the generation that we should assign to candidates that we are + * currently advertising. + */ + protected int getCurrentGeneration() + { + return currentGeneration; + } + + /** + * Increments the generation that we are assigning candidates. + */ + protected void incrementGeneration() + { + currentGeneration++; + } + + /** + * Returns the <tt>InetAddress</tt> that is most likely to be to be used + * as a next hop when contacting the specified <tt>destination</tt>. This is + * an utility method that is used whenever we have to choose one of our + * local addresses to put in the Via, Contact or (in the case of no + * registrar accounts) From headers. + * + * @param peer the CallPeer that we would contact. + * + * @return the <tt>InetAddress</tt> that is most likely to be to be used + * as a next hop when contacting the specified <tt>destination</tt>. + * + * @throws IllegalArgumentException if <tt>destination</tt> is not a valid + * host/IP/FQDN + */ + @Override + protected InetAddress getIntendedDestination(CallPeerGTalkImpl peer) + { + return peer.getProtocolProvider().getNextHop(); + } + + /** + * Creates the ICE agent that we would be using in this transport manager + * for all negotiation. + * + * @return the ICE agent to use for all the ICE negotiation that this + * transport manager would be going through + */ + private Agent createIceAgent() + { + CallPeerGTalkImpl peer = getCallPeer(); + Agent agent = null; + + /* XXX wait changes from ice4j + agent = new Agent(CompatibilityMode.GTALK); + agent.setControlling(!peer.isInitiator()); + */ + + /* XXX no configured STUN/TURN for the moment + * it should be discovered by a Google XMPP extension + for(StunServerDescriptor desc : accID.getStunServers()) + { + TransportAddress addr = new TransportAddress( + desc.getAddress(), desc.getPort(), Transport.UDP); + + StunCandidateHarvester harvester; + + + if(desc.isTurnSupported()) + { + //Yay! a TURN server + harvester + = new TurnCandidateHarvester( + addr, + new LongTermCredential( + desc.getUsername(), + desc.getPassword())); + } + else + { + //this is a STUN only server + harvester = new StunCandidateHarvester(addr); + } + + if (logger.isInfoEnabled()) + logger.info("Adding pre-configured harvester " + harvester); + + atLeastOneStunServer = true; + agent.addCandidateHarvester(harvester); + } + + if(accID.isUPNPEnabled()) + { + UPNPHarvester harvester = new UPNPHarvester(); + + if(harvester != null) + { + agent.addCandidateHarvester(harvester); + } + } + */ + + return agent; + } + + /** + * Initializes a new <tt>StreamConnector</tt> to be used as the + * <tt>connector</tt> of the <tt>MediaStream</tt> with a specific + * <tt>MediaType</tt>. + * + * @param mediaType the <tt>MediaType</tt> of the <tt>MediaStream</tt> which + * is to have its <tt>connector</tt> set to the returned + * <tt>StreamConnector</tt> + * @return a new <tt>StreamConnector</tt> to be used as the + * <tt>connector</tt> of the <tt>MediaStream</tt> with the specified + * <tt>MediaType</tt> + * @throws OperationFailedException if anything goes wrong while + * initializing the new <tt>StreamConnector</tt> + */ + @Override + protected StreamConnector createStreamConnector(MediaType mediaType) + throws OperationFailedException + { + DatagramSocket[] streamConnectorSockets + = getStreamConnectorSockets(mediaType); + + /* + * XXX If the iceAgent has not completed (yet), go with a default + * StreamConnector (until it completes). + */ + return + (streamConnectorSockets == null) + ? super.createStreamConnector(mediaType) + : new DefaultStreamConnector( + streamConnectorSockets[0 /* RTP */], + streamConnectorSockets[1 /* RTCP */]); + } + + /** + * Gets the <tt>StreamConnector</tt> to be used as the <tt>connector</tt> of + * the <tt>MediaStream</tt> with a specific <tt>MediaType</tt>. + * + * @param mediaType the <tt>MediaType</tt> of the <tt>MediaStream</tt> which + * is to have its <tt>connector</tt> set to the returned + * <tt>StreamConnector</tt> + * @return the <tt>StreamConnector</tt> to be used as the <tt>connector</tt> + * of the <tt>MediaStream</tt> with the specified <tt>MediaType</tt> + * @throws OperationFailedException if anything goes wrong while + * initializing the requested <tt>StreamConnector</tt> + * @see net.java.sip.communicator.service.protocol.media.TransportManager# + * getStreamConnector(MediaType) + */ + @Override + public StreamConnector getStreamConnector(MediaType mediaType) + throws OperationFailedException + { + StreamConnector streamConnector = super.getStreamConnector(mediaType); + + /* + * Since the super caches the StreamConnectors, make sure that the + * returned one is up-to-date with the iceAgent. + */ + if (streamConnector != null) + { + DatagramSocket[] streamConnectorSockets + = getStreamConnectorSockets(mediaType); + + /* + * XXX If the iceAgent has not completed (yet), go with the default + * StreamConnector (until it completes). + */ + if ((streamConnectorSockets != null) + && ((streamConnector.getDataSocket() + != streamConnectorSockets[0 /* RTP */]) + )) + //|| (streamConnector.getControlSocket() + // != streamConnectorSockets[1 /* RTCP */]))) + { + // Recreate the StreamConnector for the specified mediaType. + closeStreamConnector(mediaType); + streamConnector = super.getStreamConnector(mediaType); + } + } + + return streamConnector; + } + + /** + * Gets an array of <tt>DatagramSocket</tt>s which represents the sockets to + * be used by the <tt>StreamConnector</tt> with the specified + * <tt>MediaType</tt> in the order of {@link #COMPONENT_IDS} if + * {@link #iceAgent} has completed. + * + * @param mediaType the <tt>MediaType</tt> of the <tt>StreamConnector</tt> + * for which the <tt>DatagramSocket</tt>s are to be returned + * @return an array of <tt>DatagramSocket</tt>s which represents the sockets + * to be used by the <tt>StreamConnector</tt> which the specified + * <tt>MediaType</tt> in the order of {@link #COMPONENT_IDS} if + * {@link #iceAgent} has completed; otherwise, <tt>null</tt> + */ + private DatagramSocket[] getStreamConnectorSockets(MediaType mediaType) + { + String mediaName = null; + + if(mediaType == MediaType.AUDIO) + { + mediaName = "rtp"; + } + else if(mediaType == MediaType.VIDEO) + { + mediaName = "video_rtp"; + } + else + { + logger.error("Not an audio/rtp mediatype"); + return null; + } + + IceMediaStream stream = iceAgent.getStream(mediaName); + + if (stream != null) + { + DatagramSocket[] streamConnectorSockets + = new DatagramSocket[COMPONENT_IDS.length]; + int streamConnectorSocketCount = 0; + + for (int i = 0; i < COMPONENT_IDS.length; i++) + { + Component component = stream.getComponent(COMPONENT_IDS[i]); + + if (component != null) + { + CandidatePair selectedPair = component.getSelectedPair(); + + if (selectedPair != null) + { + DatagramSocket streamConnectorSocket + = selectedPair.getLocalCandidate().getSocket(); + + if (streamConnectorSocket != null) + { + streamConnectorSockets[i] = streamConnectorSocket; + streamConnectorSocketCount++; + } + } + } + } + if (streamConnectorSocketCount > 0) + { + // XXX GTalk audio has not RTCP channel + if(mediaName.equals("rtp") && streamConnectorSocketCount == 1) + { + try + { + streamConnectorSockets[1] = new DatagramSocket(); + } + catch(Exception e) + { + } + } + + return streamConnectorSockets; + } + } + return null; + } + + /** + * Implements {@link TransportManagerJabberImpl#getStreamTarget(MediaType)}. + * Gets the <tt>MediaStreamTarget</tt> to be used as the <tt>target</tt> of + * the <tt>MediaStream</tt> with a specific <tt>MediaType</tt>. + * + * @param mediaType the <tt>MediaType</tt> of the <tt>MediaStream</tt> which + * is to have its <tt>target</tt> set to the returned + * <tt>MediaStreamTarget</tt> + * @return the <tt>MediaStreamTarget</tt> to be used as the <tt>target</tt> + * of the <tt>MediaStream</tt> with the specified <tt>MediaType</tt> + * @see TransportManagerJabberImpl#getStreamTarget(MediaType) + */ + public MediaStreamTarget getStreamTarget(MediaType mediaType) + { + IceMediaStream stream = null; + MediaStreamTarget streamTarget = null; + String mediaName = null; + + if(mediaType == MediaType.AUDIO) + { + mediaName = "rtp"; + } + else if(mediaType == MediaType.VIDEO) + { + mediaName = "video_rtp"; + } + else + { + logger.error("Not an audio/rtp mediatype"); + return null; + } + + stream = iceAgent.getStream(mediaName); + + if (stream != null) + { + InetSocketAddress[] streamTargetAddresses + = new InetSocketAddress[COMPONENT_IDS.length]; + int streamTargetAddressCount = 0; + + for (int i = 0; i < COMPONENT_IDS.length; i++) + { + Component component = stream.getComponent(COMPONENT_IDS[i]); + + if (component != null) + { + CandidatePair selectedPair = component.getSelectedPair(); + + if (selectedPair != null) + { + InetSocketAddress streamTargetAddress + = selectedPair + .getRemoteCandidate() + .getTransportAddress(); + + if (streamTargetAddress != null) + { + streamTargetAddresses[i] = streamTargetAddress; + streamTargetAddressCount++; + } + } + } + } + if (streamTargetAddressCount > 0) + { + int rtcpIndex = 1; + + // XXX GTalk audio has not RTCP channel + if(mediaName.equals("rtp") && streamTargetAddressCount == 1) + { + rtcpIndex = 0; + } + + streamTarget + = new MediaStreamTarget( + streamTargetAddresses[0 /* RTP */], + streamTargetAddresses[rtcpIndex /* RTCP */]); + } + } + return streamTarget; + } + + /** + * Creates an {@link IceMediaStream} with the specified <tt>media</tt> + * name. + * + * @param media the name of the stream we'd like to create. + * + * @return the newly created {@link IceMediaStream} + * + * @throws OperationFailedException if binding on the specified media stream + * fails for some reason. + */ + private IceMediaStream createIceStream(String media) + throws OperationFailedException + { + IceMediaStream stream; + + try + { + //the following call involves STUN processing so it may take a while + stream = getNetAddrMgr().createIceStream( + nextMediaPortToTry, media, iceAgent); + } + catch (Exception ex) + { + throw new OperationFailedException( + "Failed to initialize stream " + media, + OperationFailedException.INTERNAL_ERROR, + ex); + } + + //let's now update the next port var as best we can: we would assume + //that all local candidates are bound on the same port and set it + //to the one just above. if the assumption is wrong the next bind + //would simply include one more bind retry. + try + { + nextMediaPortToTry = stream.getComponent(Component.RTCP) + .getLocalCandidates().get(0) + .getTransportAddress().getPort() + 1; + } + catch(Throwable t) + { + //hey, we were just trying to be nice. if that didn't work for + //some reason we really can't be held responsible! + logger.debug("Determining next port didn't work: ", t); + } + + return stream; + } + + /** + * Returns a reference to the {@link NetworkAddressManagerService}. The only + * reason this method exists is that {@link JabberActivator + * #getNetworkAddressManagerService()} is too long to write and makes code + * look clumsy. + * + * @return a reference to the {@link NetworkAddressManagerService}. + */ + private static NetworkAddressManagerService getNetAddrMgr() + { + return JabberActivator.getNetworkAddressManagerService(); + } + + /** + * Starts transport candidate harvest. This method should complete rapidly + * and, in case of lengthy procedures like STUN/TURN/UPnP candidate harvests + * are necessary, they should be executed in a separate thread. + * + * @param ourAnswer the content descriptions that we should be adding our + * transport lists to (although not necessarily in this very instance). + * @param candidatesSender the <tt>CandidatesSender</tt> to be used by + * this <tt>TransportManagerGTalkImpl</tt> to send <tt>candidates</tt> + * <tt>SessionIQ</tt>s from the local peer to the remote peer if this + * <tt>TransportManagerGTalkImpl</tt> wishes to utilize + * <tt>candidates</tt>. + * + * @throws OperationFailedException if we fail to allocate a port number. + */ + public void startCandidateHarvest( + List<PayloadTypePacketExtension> ourAnswer, + CandidatesSender candidatesSender) + throws OperationFailedException + { + boolean audio = false; + boolean video = false; + List<GTalkCandidatePacketExtension> candidates + = new LinkedList<GTalkCandidatePacketExtension>(); + + for(PayloadTypePacketExtension ext : ourAnswer) + { + if(ext.getNamespace().equals( + SessionIQProvider.GTALK_AUDIO_NAMESPACE)) + { + audio = true; + } + else if(ext.getNamespace().equals( + SessionIQProvider.GTALK_VIDEO_NAMESPACE)) + { + video = true; + } + } + + if(audio) + { + IceMediaStream stream = createIceStream("rtp"); + + /* remove RTCP component for the audio as it is not used and + * remote gmail peer does not send them + */ + for(Component cmp : stream.getComponents()) + { + if(cmp.getComponentID() == 2) + { + stream.removeComponent(cmp); + } + } + + candidates.addAll(GTalkPacketFactory.createCandidates("rtp", + stream)); + } + + if(video) + { + IceMediaStream stream = createIceStream("video_rtp"); + candidates.addAll(GTalkPacketFactory.createCandidates("video_rtp", + stream)); + } + + /* send candidates */ + candidatesSender.sendCandidates(candidates); + } + + /** + * Notifies the transport manager that it should conclude candidate + * harvesting as soon as possible. + */ + public void wrapupCandidateHarvest() + { + } + + /** + * Starts the connectivity establishment of this + * <tt>TransportManagerGTalkImpl</tt> i.e. checks the connectivity between + * the local and the remote peers given the remote counterpart of the + * negotiation between them. + * + * @param remote the collection of <tt>CandidatePacketExtension</tt>s which + * represents the remote counterpart of the negotiation between the local + * and the remote peer + * @return <tt>true</tt> if connectivity establishment has been started in + * response to the call; otherwise, <tt>false</tt>. + */ + public boolean startConnectivityEstablishment( + Iterable<GTalkCandidatePacketExtension> remote) + { + /* If ICE is already running, we try to update the checklists with + * the candidates. Note that this is a best effort. + */ + if (IceProcessingState.RUNNING.equals(iceAgent.getState())) + { + if(logger.isInfoEnabled()) + { + logger.info("Update Google ICE remote candidates"); + } + + if(remote == null) + { + return false; + } + + for(GTalkCandidatePacketExtension candidate : remote) + { + String name = candidate.getName(); + int numComponent = 0; + + // change name to retrieve properly the ICE media stream + if(name.equals("rtp")) + { + numComponent = 1; + } + else if(name.equals("rtcp")) + { + name = "rtp"; + numComponent = 1; + } + else if(name.equals("video_rtp")) + { + numComponent = 1; + } + else if(name.equals("video_rtcp")) + { + name = "video_rtp"; + numComponent = 2; + } + + IceMediaStream stream = iceAgent.getStream(name); + + if(stream == null) + { + continue; + } + + /* Different candidates may have different ufrag/password */ + String ufrag = candidate.getUsername(); + //String password = candidate.getPassword(); + + /* + * Is the remote candidate from the current generation of + * the iceAgent? + */ + if (candidate.getGeneration() != iceAgent.getGeneration()) + continue; + + // XXX UDP only for the moment as ice4j.org does not support + // TCP yet + if(!candidate.getProtocol().equalsIgnoreCase( + Transport.UDP.toString())) + continue; + + Component component + = stream.getComponent(numComponent); + /* XXX wait changes from ice4j + RemoteCandidate remoteCandidate = new RemoteCandidate( + new TransportAddress( + candidate.getAddress(), + candidate.getPort(), + Transport.parse( + candidate.getProtocol())), + component, + org.ice4j.ice.CandidateType.parse( + candidate.getType().toString()), + "0", + (long)(candidate.getPreference() * 1000), + ufrag); + component.addUpdateRemoteCandidate(remoteCandidate); + */ + } + + /* update all components of all streams */ + for(IceMediaStream stream : iceAgent.getStreams()) + { + for(Component component : stream.getComponents()) + { + component.updateRemoteCandidate(); + } + } + return false; + } + + int generation = iceAgent.getGeneration(); + boolean startConnectivityEstablishment = false; + + if(remote == null) + { + return false; + } + + for(GTalkCandidatePacketExtension candidate : remote) + { + String name = candidate.getName(); + int numComponent = 0; + + // change name to retrieve properly the ICE media stream + if(name.equals("rtp")) + { + numComponent = 1; + } + else if(name.equals("rtcp")) + { + name = "rtp"; + numComponent = 1; + } + else if(name.equals("video_rtp")) + { + numComponent = 1; + } + else if(name.equals("video_rtcp")) + { + name = "video_rtp"; + numComponent = 2; + } + + IceMediaStream stream = iceAgent.getStream(name); + + if(stream == null) + { + continue; + } + + /* Different candidates may have different ufrag/password */ + String ufrag = candidate.getUsername(); + //String password = candidate.getPassword(); + + /* + * Is the remote candidate from the current generation of + * the iceAgent? + */ + if (candidate.getGeneration() != generation) + continue; + + if(!candidate.getProtocol().equalsIgnoreCase( + Transport.UDP.toString())) + continue; + + Component component + = stream.getComponent(numComponent); + + /* XXX wait changes from ice4j + RemoteCandidate remoteCandidate = new RemoteCandidate( + new TransportAddress( + candidate.getAddress(), + candidate.getPort(), + Transport.parse( + candidate.getProtocol())), + component, + org.ice4j.ice.CandidateType.parse( + candidate.getType().toString()), + "0", + (long)(candidate.getPreference() * 1000), + ufrag); + component.addRemoteCandidate(remoteCandidate); + */ + startConnectivityEstablishment = true; + } + + if (startConnectivityEstablishment) + { + /* + * Once again because the ICE Agent does not support adding + * candidates after the connectivity establishment has been started + * and because multiple transport-info JingleIQs may be used to send + * the whole set of transport candidates from the remote peer to the + * local peer, do not really start the connectivity establishment + * until we have at least two remote candidates (i.e. local and + * stun) per ICE Component. + */ + for (IceMediaStream stream : iceAgent.getStreams()) + { + for (Component component : stream.getComponents()) + { + if(component.getName().equals("RTCP")) + continue; + if (component.getRemoteCandidateCount() < 2) + { + startConnectivityEstablishment = false; + break; + } + } + if (!startConnectivityEstablishment) + break; + } + + if (startConnectivityEstablishment) + { + iceAgent.startConnectivityEstablishment(); + return true; + } + } + return false; + } + + /** + * Waits for the associated ICE <tt>Agent</tt> to finish any started + * connectivity checks. + * + * @throws OperationFailedException if ICE processing has failed + */ + public void wrapupConnectivityEstablishment() + throws OperationFailedException + { + final Object iceProcessingStateSyncRoot = new Object(); + PropertyChangeListener stateChangeListener + = new PropertyChangeListener() + { + public void propertyChange(PropertyChangeEvent evt) + { + Object newValue = evt.getNewValue(); + + if (IceProcessingState.COMPLETED.equals(newValue) + || IceProcessingState.FAILED.equals(newValue) + || IceProcessingState.TERMINATED.equals(newValue)) + { + if (logger.isTraceEnabled()) + logger.trace("ICE " + newValue); + + Agent iceAgent = (Agent) evt.getSource(); + + iceAgent.removeStateChangeListener(this); + + if (iceAgent == TransportManagerGTalkImpl.this.iceAgent) + { + synchronized (iceProcessingStateSyncRoot) + { + iceProcessingStateSyncRoot.notify(); + } + } + } + } + }; + + iceAgent.addStateChangeListener(stateChangeListener); + + // Wait for the connectivity checks to finish if they have been started. + boolean interrupted = false; + + synchronized (iceProcessingStateSyncRoot) + { + while (IceProcessingState.RUNNING.equals(iceAgent.getState())) + { + try + { + iceProcessingStateSyncRoot.wait(); + } + catch (InterruptedException ie) + { + interrupted = true; + } + } + } + + if (interrupted) + Thread.currentThread().interrupt(); + + /* + * Make sure stateChangeListener is removed from iceAgent in case its + * #propertyChange(PropertyChangeEvent) has never been executed. + */ + iceAgent.removeStateChangeListener(stateChangeListener); + + /* check the state of ICE processing and throw exception if failed */ + if(iceAgent.getState().equals(IceProcessingState.FAILED)) + { + throw new OperationFailedException( + "Could not establish connection (ICE failed)", + OperationFailedException.GENERAL_ERROR); + } + } + + /** + * Close this transport manager and release resources. + */ + public void close() + { + if(iceAgent != null) + { + iceAgent.free(); + } + } +} diff --git a/src/net/java/sip/communicator/service/protocol/media/AbstractOperationSetTelephonyConferencing.java b/src/net/java/sip/communicator/service/protocol/media/AbstractOperationSetTelephonyConferencing.java index 2095fee..4d6fc4e 100644 --- a/src/net/java/sip/communicator/service/protocol/media/AbstractOperationSetTelephonyConferencing.java +++ b/src/net/java/sip/communicator/service/protocol/media/AbstractOperationSetTelephonyConferencing.java @@ -245,7 +245,7 @@ public abstract class AbstractOperationSetTelephonyConferencing< * @throws OperationFailedException if inviting the specified callee to the * specified call fails */ - protected abstract MediaAwareCallPeerT inviteCalleeToCall( + protected abstract CallPeer inviteCalleeToCall( CalleeAddressT calleeAddress, MediaAwareCallT call, boolean wasConferenceFocus) diff --git a/src/net/java/sip/communicator/service/protocol/media/MediaAwareCall.java b/src/net/java/sip/communicator/service/protocol/media/MediaAwareCall.java index d5ac2c0..d3b1261 100644 --- a/src/net/java/sip/communicator/service/protocol/media/MediaAwareCall.java +++ b/src/net/java/sip/communicator/service/protocol/media/MediaAwareCall.java @@ -520,6 +520,16 @@ public abstract class MediaAwareCall< } /** + * Get the media use case. + * + * @return media use case + */ + public MediaUseCase getMediaUseCase() + { + return mediaUseCase; + } + + /** * Determines whether the streaming of local video in this <tt>Call</tt> * is currently allowed. The setting does not reflect the availability of * actual video capture devices, it just expresses the local policy (or |