diff --git a/jingle/extension/build/build.properties b/jingle/extension/build/build.properties new file mode 100644 index 000000000..a15542f03 --- /dev/null +++ b/jingle/extension/build/build.properties @@ -0,0 +1 @@ +jdk.home.1.5=C:/Arquivos de programas/Java/jdk1.5.0_09 \ No newline at end of file diff --git a/jingle/extension/build/build.xml b/jingle/extension/build/build.xml new file mode 100644 index 000000000..f7e950a54 --- /dev/null +++ b/jingle/extension/build/build.xml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jingle/extension/build/lib/junit.jar b/jingle/extension/build/lib/junit.jar new file mode 100644 index 000000000..02ac8fb5b Binary files /dev/null and b/jingle/extension/build/lib/junit.jar differ diff --git a/jingle/extension/build/lib/smack.jar b/jingle/extension/build/lib/smack.jar new file mode 100644 index 000000000..8576e5b19 Binary files /dev/null and b/jingle/extension/build/lib/smack.jar differ diff --git a/jingle/extension/build/lib/smackx-debug.jar b/jingle/extension/build/lib/smackx-debug.jar new file mode 100644 index 000000000..64336e767 Binary files /dev/null and b/jingle/extension/build/lib/smackx-debug.jar differ diff --git a/jingle/extension/build/lib/smackx.jar b/jingle/extension/build/lib/smackx.jar new file mode 100644 index 000000000..4c7f358ba Binary files /dev/null and b/jingle/extension/build/lib/smackx.jar differ diff --git a/jingle/extension/build/merge/jstun-0.6.1.jar b/jingle/extension/build/merge/jstun-0.6.1.jar new file mode 100644 index 000000000..f2a5f1faa Binary files /dev/null and b/jingle/extension/build/merge/jstun-0.6.1.jar differ diff --git a/jingle/extension/build/projects/Jingle.ipr b/jingle/extension/build/projects/Jingle.ipr new file mode 100644 index 000000000..425e6dc42 --- /dev/null +++ b/jingle/extension/build/projects/Jingle.ipr @@ -0,0 +1,315 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jingle/extension/build/projects/Jingle.iws b/jingle/extension/build/projects/Jingle.iws new file mode 100644 index 000000000..7203abc19 --- /dev/null +++ b/jingle/extension/build/projects/Jingle.iws @@ -0,0 +1,635 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + localhost + 5050 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jingle/extension/build/projects/JingleExtension.iml b/jingle/extension/build/projects/JingleExtension.iml new file mode 100644 index 000000000..8a458a56a --- /dev/null +++ b/jingle/extension/build/projects/JingleExtension.iml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jingle/extension/build/projects/Smack.iws b/jingle/extension/build/projects/Smack.iws new file mode 100644 index 000000000..698d5c636 --- /dev/null +++ b/jingle/extension/build/projects/Smack.iws @@ -0,0 +1,593 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + localhost + 5050 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/IncomingJingleSession.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/IncomingJingleSession.java new file mode 100644 index 000000000..51210ce26 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/IncomingJingleSession.java @@ -0,0 +1,421 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright (C) 2002-2006 Jive Software. All rights reserved. + * ==================================================================== + * The Jive Software License (based on Apache Software License, Version 1.1) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by + * Jive Software (http://www.jivesoftware.com)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Smack" and "Jive Software" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please + * contact webmaster@jivesoftware.com. + * + * 5. Products derived from this software may not be called "Smack", + * nor may "Smack" appear in their name, without prior written + * permission of Jive Software. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL JIVE SOFTWARE OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + */ + +package org.jivesoftware.smackx.jingle; + +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.jingle.listeners.JingleMediaListener; +import org.jivesoftware.smackx.jingle.listeners.JingleTransportListener; +import org.jivesoftware.smackx.jingle.media.JingleMediaManager; +import org.jivesoftware.smackx.jingle.media.MediaNegotiator; +import org.jivesoftware.smackx.jingle.media.PayloadType; +import org.jivesoftware.smackx.jingle.nat.TransportCandidate; +import org.jivesoftware.smackx.jingle.nat.TransportNegotiator; +import org.jivesoftware.smackx.jingle.nat.TransportResolver; +import org.jivesoftware.smackx.packet.Jingle; +import org.jivesoftware.smackx.packet.JingleContentDescription; +import org.jivesoftware.smackx.packet.JingleContentDescription.JinglePayloadType; +import org.jivesoftware.smackx.packet.JingleError; + +import java.util.List; + +/** + * An incoming Jingle Session implementation. + * This class has especific bahavior to accept and establish a received Jingle Session Request. + * + * This class is not directly used by users. Instead, users should refer to the + * JingleManager class, that will create the appropiate instance... + * + * @author Alvaro Saurin + */ +public class IncomingJingleSession extends JingleSession { + + // states + private final Accepting accepting; + + private final Pending pending; + + private final Active active; + + private JingleSessionRequest initialSessionRequest; + + /** + * Constructor for a Jingle incoming session + * + * @param conn the XMPP connection + * @param responder the responder + * @param resolver The transport resolver + */ + protected IncomingJingleSession(XMPPConnection conn, String responder, + List payloadTypes, TransportResolver resolver) { + + super(conn, responder, conn.getUser()); + + // Create the states... + + accepting = new Accepting(this); + pending = new Pending(this); + active = new Active(this); + + setMediaNeg(new MediaNegotiator(this, payloadTypes)); + if (resolver.getType().equals(TransportResolver.Type.rawupd)) { + setTransportNeg(new TransportNegotiator.RawUdp(this, resolver)); + } + if (resolver.getType().equals(TransportResolver.Type.ice)) { + setTransportNeg(new TransportNegotiator.Ice(this, resolver)); + } + } + + /** + * Constructor for a Jingle Incoming session with a defined Media Manager + * + * @param conn the XMPP connection + * @param responder the responder + * @param resolver The transport resolver + * @param jingleMediaManager The Media Manager for this Session + */ + protected IncomingJingleSession(XMPPConnection conn, String responder, + List payloadTypes, TransportResolver resolver, JingleMediaManager jingleMediaManager) { + this(conn, responder, payloadTypes, resolver); + this.jingleMediaManager = jingleMediaManager; + } + + /** + * Start the session for a initial Jingle request packet. + * + * @param initialJingleSessionRequest the initial Jingle Session Request + * @throws XMPPException + */ + public void start(JingleSessionRequest initialJingleSessionRequest) throws XMPPException { + if (invalidState()) { + Jingle packet = initialJingleSessionRequest.getJingle(); + System.out.println("invalidState"); + if (packet != null) { + + // Initialize the session information + setSid(packet.getSid()); + + // Establish the default state + setState(accepting); + + updatePacketListener(); + respond(packet); + } else { + throw new IllegalStateException( + "Session request with null Jingle packet."); + } + } else { + throw new IllegalStateException("Starting session without null state."); + } + } + + /** + * Start the session using initial Jingle Session Request used to create this session.. + * + * @throws XMPPException + */ + public void start() throws XMPPException { + start(this.getInitialSessionRequest()); + } + + /** + * Get the initial Jingle packet request + * + * @return + */ + JingleSessionRequest getInitialSessionRequest() { + return initialSessionRequest; + } + + /** + * Get the initial Jingle packet request + * + * @param initialRequest the initial Jingle packet + */ + void setInitialSessionRequest(JingleSessionRequest initialRequest) { + this.initialSessionRequest = initialRequest; + } + + // States + + /** + * First stage when we have received a session request, and we accept the + * request. We start in this stage, as the instance is created when the user + * accepts the connection... + */ + public class Accepting extends JingleNegotiator.State { + + public Accepting(JingleNegotiator neg) { + super(neg); + } + + /** + * Initiate the incoming session. We have already sent the ACK partially + * accepting the session... + * + * @throws XMPPException + */ + public Jingle eventInitiate(Jingle inJingle) throws XMPPException { + // Set the new session state + setState(pending); + return super.eventInitiate(inJingle); + } + + /** + * An error has occurred. + * + * @throws XMPPException + */ + public void eventError(IQ iq) throws XMPPException { + triggerSessionClosedOnError(new JingleException(iq.getError().getMessage())); + super.eventError(iq); + } + } + + /** + * "Pending" state: we are waiting for the transport and content + * negotiators. + */ + private class Pending extends JingleNegotiator.State { + + JingleMediaListener jingleMediaListener; + + JingleTransportListener jingleTransportListener; + + public Pending(JingleNegotiator neg) { + super(neg); + + // Create the listeners that will send a "session-accept" when the + // sub-negotiators are done. + jingleMediaListener = new JingleMediaListener() { + public void mediaClosed(PayloadType cand) { + } + + public void mediaEstablished(PayloadType pt) { + checkFullyEstablished(); + } + }; + + jingleTransportListener = new JingleTransportListener() { + public void transportEstablished(TransportCandidate local, + TransportCandidate remote) { + checkFullyEstablished(); + } + + public void transportClosed(TransportCandidate cand) { + } + + public void transportClosedOnError(XMPPException e) { + } + }; + } + + /** + * Enter in the pending state: wait for the sub-negotiators. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter() + */ + public void eventEnter() { + // Add the listeners to the sub-negotiators... + System.out.println("Pending eventEnter"); + addMediaListener(jingleMediaListener); + addTransportListener(jingleTransportListener); + super.eventEnter(); + } + + /** + * Exit of the state + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventExit() + */ + public void eventExit() { + removeMediaListener(jingleMediaListener); + removeTransportListener(jingleTransportListener); + super.eventExit(); + } + + /** + * Check if the session has been fully accepted by all the + * sub-negotiators and, in that case, send an "accept" message... + */ + private void checkFullyEstablished() { + if (isFullyEstablished()) { + + PayloadType.Audio bestCommonAudioPt = getMediaNeg() + .getBestCommonAudioPt(); + TransportCandidate bestRemoteCandidate = getTransportNeg() + .getBestRemoteCandidate(); + TransportCandidate acceptedLocalCandidate = getTransportNeg() + .getAcceptedLocalCandidate(); + + if (bestCommonAudioPt != null && bestRemoteCandidate != null + && acceptedLocalCandidate != null) { + // Ok, send a packet saying that we accept this session + Jingle jout = new Jingle(Jingle.Action.SESSIONACCEPT); + + // ... with the audio payload type and the transport + // candidate + jout.addDescription(new JingleContentDescription.Audio( + new JinglePayloadType(bestCommonAudioPt))); + jout.addTransport(getTransportNeg().getJingleTransport( + bestRemoteCandidate)); + + addExpectedId(jout.getPacketID()); + sendFormattedJingle(jout); + } + } + } + + /** + * The other endpoint has accepted the session. + */ + public Jingle eventAccept(Jingle jin) throws XMPPException { + + PayloadType acceptedPayloadType = null; + TransportCandidate acceptedLocalCandidate = null; + + // We process the "accepted" if we have finished the + // sub-negotiators. Maybe this is not needed (ie, the other endpoint + // can take the first valid transport candidate), but otherwise we + // must cancel the negotiators... + // + if (isFullyEstablished()) { + acceptedPayloadType = getAcceptedAudioPayloadType(jin); + acceptedLocalCandidate = getAcceptedLocalCandidate(jin); + + if (acceptedPayloadType != null && acceptedLocalCandidate != null) { + if (acceptedPayloadType.equals(getMediaNeg().getBestCommonAudioPt()) + && acceptedLocalCandidate.equals(getTransportNeg() + .getAcceptedLocalCandidate())) { + setState(active); + } + } else { + throw new JingleException(JingleError.MALFORMED_STANZA); + } + } + + return super.eventAccept(jin); + } + + /** + * We have received a confirmation. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ) + */ + public Jingle eventAck(IQ iq) throws XMPPException { + setState(active); + return super.eventAck(iq); + } + + /** + * An error has occurred. + * + * @throws XMPPException + */ + public void eventError(IQ iq) throws XMPPException { + if (iq == null) return; + triggerSessionClosedOnError(new XMPPException(iq.getError())); + super.eventError(iq); + } + } + + /** + * "Active" state: we have an agreement about the session. + */ + private class Active extends JingleNegotiator.State { + public Active(JingleNegotiator neg) { + super(neg); + } + + /** + * We have a established session: notify the listeners + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter() + */ + public void eventEnter() { + PayloadType.Audio bestCommonAudioPt = getMediaNeg().getBestCommonAudioPt(); + TransportCandidate bestRemoteCandidate = getTransportNeg() + .getBestRemoteCandidate(); + TransportCandidate acceptedLocalCandidate = getTransportNeg() + .getAcceptedLocalCandidate(); + + // Trigger the session established flag + System.out.println("eventEntered"); + triggerSessionEstablished(bestCommonAudioPt, bestRemoteCandidate, + acceptedLocalCandidate); + + super.eventEnter(); + } + + /** + * Terminate the connection. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventTerminate(org.jivesoftware.smackx.packet.Jingle) + */ + public Jingle eventTerminate(Jingle jin) throws XMPPException { + triggerSessionClosed("Closed Remotely"); + return super.eventTerminate(jin); + } + + /** + * An error has occurred. + * + * @throws XMPPException + */ + public void eventError(IQ iq) throws XMPPException { + triggerSessionClosedOnError(new XMPPException(iq.getError().getMessage())); + super.eventError(iq); + } + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleManager.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleManager.java new file mode 100644 index 000000000..5ebdf94df --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleManager.java @@ -0,0 +1,703 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2005 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.jingle; + +import org.jivesoftware.smack.*; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.provider.ProviderManager; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.jingle.listeners.CreatedJingleSessionListener; +import org.jivesoftware.smackx.jingle.listeners.JingleListener; +import org.jivesoftware.smackx.jingle.listeners.JingleSessionListener; +import org.jivesoftware.smackx.jingle.listeners.JingleSessionRequestListener; +import org.jivesoftware.smackx.jingle.media.JingleMediaManager; +import org.jivesoftware.smackx.jingle.media.PayloadType; +import org.jivesoftware.smackx.jingle.nat.BasicResolver; +import org.jivesoftware.smackx.jingle.nat.JingleTransportManager; +import org.jivesoftware.smackx.jingle.nat.TransportCandidate; +import org.jivesoftware.smackx.jingle.nat.TransportResolver; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.Jingle; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Jingle is a session establishment protocol defined in (XEP-0166). + * It defines a framework for negotiating and managing out-of-band ( data that is send and receive through other connection than XMPP connection) data sessions over XMPP. + * With this protocol you can setup VOIP Calls, Video Streaming, File transfers and whatever out-of-band session based transmission. + * + * To create a Jingle Session you need a Transport method and a Payload type. + * + * A transport method is how it will trasmit and receive network packets. Transport MUST have one or more candidates. + * A transport candidate is an IP Address with a defined port, that other party must send data to. + * + * A supported payload type, is the data encoding format that the jmf will be transmitted. + * For instance an Audio Payload "GSM". + * + * A Jingle session negociates a payload type and a pair of transport candidates. + * Which means that when a Jingle Session is establhished you will have two defined transport candidates with addresses + * and a defined Payload type. + * In other words, you will have two IP address with their respective ports, and a Codec type defined. + * + * The JingleManager is a facade built upon Jabber Jingle (XEP-166) to allow the + * use of Jingle. This implementation allows the user to simply + * use this class for setting the Jingle parameters, create and receive Jingle Sessions. + * + * In order to use the Jingle, the user must provide a + * TransportManager that will handle the resolution of potential IP addresses taht can be used to transport the streaming (jmf). + * This TransportManager can be initialized with several default resolvers, + * including a fixed solver that can be used when the address and port are know + * in advance. + * This API have ready to use Transport Managers, for instance: BasicTransportManager, STUNTransportManager, BridgedTransportManager. + * + * You should also especify a JingleMediaManager if you want that JingleManager assume Media control + * Using a JingleMediaManager implementation is the easier way to implement a Jingle Application. + * + * Otherwise before creating an outgoing connection, the user must create jingle session + * listeners that will be called when different events happen. The most + * important event is sessionEstablished(), that will be called when all + * the negotiations are finished, providing the payload type for the + * transmission as well as the remote and local addresses and ports for the + * communication. See JingleSessionListener for a complete list of events that can be + * observed. + * + * This is an example of how to use the JingleManager: + * This example implements a Jingle VOIP Call between two users. + * + *
+ *
+ *                               To wait for an Incoming Jingle Session:
+ *
+ *                               try {
+ *
+ *                                           // Connect to a XMPP Server
+ *                                           XMPPConnection x1 = new XMPPConnection("xmpp.com");
+ *                                           x1.connect();
+ *                                           x1.login("juliet", "juliet");
+ *
+ *                                           // Create a JingleManager using a BasicResolver
+ *                                           final JingleManager jm1 = new JingleManager(
+ *                                                   x1, new BasicTransportManager());
+ *
+ *                                           // Create a JingleMediaManager. In this case using Jingle Audio Media API
+ *                                           JingleMediaManager jingleMediaManager = new AudioMediaManager();
+ *
+ *                                           // Set the JingleMediaManager
+ *                                           jm1.setMediaManager(jingleMediaManager);
+ *
+ *                                           // Listen for incoming calls
+ *                                           jm1.addJingleSessionRequestListener(new JingleSessionRequestListener() {
+ *                                               public void sessionRequested(JingleSessionRequest request) {
+ *
+ *                                                   try {
+ *                                                      // Accept the call
+ *                                                      IncomingJingleSession session = request.accept();
+ *
+ *
+ *                                                       // Start the call
+ *                                                       session.start();
+ *                                                   } catch (XMPPException e) {
+ *                                                       e.printStackTrace();
+ *                                                   }
+ *
+ *                                               }
+ *                                           });
+ *
+ *                                       Thread.sleep(15000);
+ *
+ *                                       } catch (Exception e) {
+ *                                           e.printStackTrace();
+ *                                       }
+ *
+ *                               To create an Outgoing Jingle Session:
+ *
+ *                                     try {
+ *
+ *                                           // Connect to a XMPP Server
+ *                                           XMPPConnection x0 = new XMPPConnection("xmpp.com");
+ *                                           x0.connect();
+ *                                           x0.login("romeo", "romeo");
+ *
+ *                                           // Create a JingleManager using a BasicResolver
+ *                                           final JingleManager jm0 = new JingleManager(
+ *                                                   x0, new BasicTransportManager());
+ *
+ *                                           // Create a JingleMediaManager. In this case using Jingle Audio Media API
+ *                                           JingleMediaManager jingleMediaManager = new AudioMediaManager(); // Using Jingle Media API
+ *
+ *                                           // Set the JingleMediaManager
+ *                                           jm0.setMediaManager(jingleMediaManager);
+ *
+ *                                           // Create a new Jingle Call with a full JID
+ *                                           OutgoingJingleSession js0 = jm0.createOutgoingJingleSession("juliet@xmpp.com/Smack");
+ *
+ *                                           // Start the call
+ *                                           js0.start();
+ *
+ *                                           Thread.sleep(10000);
+ *                                           js0.terminate();
+ *
+ *                                           Thread.sleep(3000);
+ *
+ *                                       } catch (Exception e) {
+ *                                           e.printStackTrace();
+ *                                       }
+ *                               
+ * + * @author Thiago Camargo + * @author Alvaro Saurin + * @see JingleListener + * @see TransportResolver + * @see org.jivesoftware.smackx.jingle.nat.JingleTransportManager + * @see OutgoingJingleSession + * @see IncomingJingleSession + * @see JingleMediaManager + * @see org.jivesoftware.smackx.jingle.nat.BasicTransportManager , STUNTransportManager, BridgedTransportManager, TransportResolver, BridgedResolver, ICEResolver, STUNResolver and BasicResolver. + */ +public class JingleManager implements JingleSessionListener { + + // non-static + + final List jingleSessions = new ArrayList(); + + // Listeners for manager events (ie, session requests...) + private List jingleSessionRequestListeners; + + // Listeners for created JingleSessions + private List creationListeners = new ArrayList(); + + // The XMPP connection + private XMPPConnection connection; + + // The Media Manager + private JingleMediaManager jingleMediaManager; + + // The Jingle transport manager + private final JingleTransportManager jingleTransportManager; + + static { + + ProviderManager providerManager = ProviderManager.getInstance(); + + providerManager.addIQProvider("jingle", "http://jabber.org/protocol/jingle", + new org.jivesoftware.smackx.provider.JingleProvider()); + + providerManager.addExtensionProvider("description", "http://jabber.org/protocol/jingle/description/audio", + new org.jivesoftware.smackx.provider.JingleContentDescriptionProvider.Audio()); + + providerManager.addExtensionProvider("description", "http://jabber.org/protocol/jingle/description/audio", + new org.jivesoftware.smackx.provider.JingleContentDescriptionProvider.Audio()); + + providerManager.addExtensionProvider("transport", "http://jabber.org/protocol/jingle/transport/ice", + new org.jivesoftware.smackx.provider.JingleTransportProvider.Ice()); + providerManager.addExtensionProvider("transport", "http://jabber.org/protocol/jingle/transport/raw-udp", + new org.jivesoftware.smackx.provider.JingleTransportProvider.RawUdp()); + + providerManager.addExtensionProvider("busy", "http://jabber.org/protocol/jingle/info/audio", + new org.jivesoftware.smackx.provider.JingleContentInfoProvider.Audio.Busy()); + providerManager.addExtensionProvider("hold", "http://jabber.org/protocol/jingle/info/audio", + new org.jivesoftware.smackx.provider.JingleContentInfoProvider.Audio.Hold()); + providerManager.addExtensionProvider("mute", "http://jabber.org/protocol/jingle/info/audio", + new org.jivesoftware.smackx.provider.JingleContentInfoProvider.Audio.Mute()); + providerManager.addExtensionProvider("queued", "http://jabber.org/protocol/jingle/info/audio", + new org.jivesoftware.smackx.provider.JingleContentInfoProvider.Audio.Queued()); + providerManager.addExtensionProvider("ringing", "http://jabber.org/protocol/jingle/info/audio", + new org.jivesoftware.smackx.provider.JingleContentInfoProvider.Audio.Ringing()); + + // Enable the Jingle support on every established connection + // The ServiceDiscoveryManager class should have been already + // initialized + XMPPConnection.addConnectionCreationListener(new ConnectionCreationListener() { + public void connectionCreated(XMPPConnection connection) { + JingleManager.setServiceEnabled(connection, true); + } + }); + } + + /** + * Default constructor with a defined XMPPConnection, Transport Resolver and a Media Manager + * If a fully implemented JingleMediaSession is entered, JingleManager manage Jingle signalling and jmf + * + * @param connection XMPP Connection to be used + * @param jingleTransportManager transport resolver to be used + * @param jingleMediaManager an implemeted JingleMediaManager to be used. + */ + public JingleManager(XMPPConnection connection, JingleTransportManager jingleTransportManager, JingleMediaManager jingleMediaManager) { + this.connection = connection; + this.jingleTransportManager = jingleTransportManager; + this.jingleMediaManager = jingleMediaManager; + + connection.getRoster().addRosterListener(new RosterListener() { + + public void entriesAdded(Collection addresses) { + } + + public void entriesUpdated(Collection addresses) { + } + + public void entriesDeleted(Collection addresses) { + } + + public void presenceChanged(String XMPPAddress) { + JingleSession aux = null; + for (JingleSession jingleSession : jingleSessions) { + if (jingleSession.getInitiator().equals(XMPPAddress) || jingleSession.getResponder().equals(XMPPAddress)) { + aux = jingleSession; + } + } + if (aux != null) + try { + aux.terminate(); + } catch (XMPPException e) { + e.printStackTrace(); + } + } + }); + + } + + /** + * Default constructor with a defined XMPPConnection and a Transport Resolver + * + * @param connection XMPP Connection to be used + * @param jingleTransportManager transport resolver to be used + */ + public JingleManager(XMPPConnection connection, JingleTransportManager jingleTransportManager) { + this(connection, jingleTransportManager, null); + } + + /** + * Default constructor with a defined XMPPConnection. + * A default JingleTransportmanager based on BasicResolver will be used in this JingleManager transport. + * + * @param connection XMPP Connection to be used + */ + public JingleManager(XMPPConnection connection) { + this(connection, new JingleTransportManager() { + protected TransportResolver createResolver() { + return new BasicResolver(); + } + }); + } + + /** + * Default constructor with a defined XMPPConnection and a defined Resolver. + * A default JingleTransportmanager based on BasicResolver will be used in this JingleManager transport. + * + * @param connection XMPP Connection to be used + */ + public JingleManager(XMPPConnection connection, final TransportResolver resolver) { + this(connection, new JingleTransportManager() { + protected TransportResolver createResolver() { + return resolver; + } + }); + } + + /** + * Enables or disables the Jingle support on a given connection. + * + * + * Before starting any Jingle jmf session, check that the user can handle + * it. Enable the Jingle support to indicate that this client handles Jingle + * messages. + * + * @param connection the connection where the service will be enabled or + * disabled + * @param enabled indicates if the service will be enabled or disabled + */ + public synchronized static void setServiceEnabled(XMPPConnection connection, + boolean enabled) { + if (isServiceEnabled(connection) == enabled) { + return; + } + + if (enabled) { + ServiceDiscoveryManager.getInstanceFor(connection).addFeature( + Jingle.NAMESPACE); + } else { + ServiceDiscoveryManager.getInstanceFor(connection).removeFeature( + Jingle.NAMESPACE); + } + } + + /** + * Returns true if the Jingle support is enabled for the given connection. + * + * @param connection the connection to look for Jingle support + * @return a boolean indicating if the Jingle support is enabled for the + * given connection + */ + public static boolean isServiceEnabled(XMPPConnection connection) { + return ServiceDiscoveryManager.getInstanceFor(connection).includesFeature( + Jingle.NAMESPACE); + } + + /** + * Returns true if the specified user handles Jingle messages. + * + * @param connection the connection to use to perform the service discovery + * @param userID the user to check. A fully qualified xmpp ID, e.g. + * jdoe@example.com + * @return a boolean indicating whether the specified user handles Jingle + * messages + */ + public static boolean isServiceEnabled(XMPPConnection connection, String userID) { + try { + DiscoverInfo result = ServiceDiscoveryManager.getInstanceFor(connection) + .discoverInfo(userID); + return result.containsFeature(Jingle.NAMESPACE); + } + catch (XMPPException e) { + e.printStackTrace(); + return false; + } + } + + /** + * Get the JingleTransportManager of this JingleManager + * + * @return + */ + public JingleTransportManager getJingleTransportManager() { + return jingleTransportManager; + } + + /** + * Get the Media Manager of this Jingle Manager + * + * @return + */ + public JingleMediaManager getMediaManager() { + return jingleMediaManager; + } + + /** + * Set the Media Manager of this Jingle Manager + * + * @param jingleMediaManager JingleMediaManager to be used for open, close, start and stop jmf streamings + */ + public void setMediaManager(JingleMediaManager jingleMediaManager) { + this.jingleMediaManager = jingleMediaManager; + } + + /** + * Add a Jingle session request listenerJingle to listen to incoming session + * requests. + * + * @param jingleSessionRequestListener an implemented JingleSessionRequestListener + * @see #removeJingleSessionRequestListener(JingleSessionRequestListener) + * @see JingleListener + */ + public synchronized void addJingleSessionRequestListener( + final JingleSessionRequestListener jingleSessionRequestListener) { + if (jingleSessionRequestListener != null) { + if (jingleSessionRequestListeners == null) { + initJingleSessionRequestListeners(); + } + synchronized (jingleSessionRequestListeners) { + jingleSessionRequestListeners.add(jingleSessionRequestListener); + } + } + } + + /** + * Removes a Jingle session listenerJingle. + * + * @param jingleSessionRequestListener The jingle session jingleSessionRequestListener to be removed + * @see #addJingleSessionRequestListener(JingleSessionRequestListener) + * @see JingleListener + */ + public void removeJingleSessionRequestListener(JingleSessionRequestListener jingleSessionRequestListener) { + if (jingleSessionRequestListeners == null) { + return; + } + synchronized (jingleSessionRequestListeners) { + jingleSessionRequestListeners.remove(jingleSessionRequestListener); + } + } + + /** + * Adds a CreatedJingleSessionListener. + * This listener will be called when a session is created by the JingleManager instance. + * + * @param createdJingleSessionListener + */ + public void addCreationListener(CreatedJingleSessionListener createdJingleSessionListener) { + this.creationListeners.add(createdJingleSessionListener); + } + + /** + * Removes a CreatedJingleSessionListener. + * This listener will be called when a session is created by the JingleManager instance. + * + * @param createdJingleSessionListener + */ + public void removeCreationListener(CreatedJingleSessionListener createdJingleSessionListener) { + this.creationListeners.remove(createdJingleSessionListener); + } + + /** + * Trigger CreatedJingleSessionListeners that a session was created. + * + * @param jingleSession + */ + public void triggerSessionCreated(JingleSession jingleSession) { + jingleSessions.add(jingleSession); + jingleSession.addListener(this); + for (CreatedJingleSessionListener createdJingleSessionListener : creationListeners) { + try { + createdJingleSessionListener.sessionCreated(jingleSession); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + public void sessionEstablished(PayloadType pt, TransportCandidate rc, TransportCandidate lc, JingleSession jingleSession) { + } + + public void sessionDeclined(String reason, JingleSession jingleSession) { + jingleSession.removeListener(this); + jingleSessions.remove(jingleSession); + jingleSession.close(); + System.err.println("Declined"); + } + + public void sessionRedirected(String redirection, JingleSession jingleSession) { + jingleSession.removeListener(this); + jingleSessions.remove(jingleSession); + } + + public void sessionClosed(String reason, JingleSession jingleSession) { + jingleSession.removeListener(this); + jingleSessions.remove(jingleSession); + } + + public void sessionClosedOnError(XMPPException e, JingleSession jingleSession) { + jingleSession.removeListener(this); + jingleSessions.remove(jingleSession); + } + + /** + * Register the listenerJingles, waiting for a Jingle packet that tries to + * establish a new session. + */ + private void initJingleSessionRequestListeners() { + PacketFilter initRequestFilter = new PacketFilter() { + // Return true if we accept this packet + public boolean accept(Packet pin) { + if (pin instanceof IQ) { + IQ iq = (IQ) pin; + if (iq.getType().equals(IQ.Type.SET)) { + if (iq instanceof Jingle) { + Jingle jin = (Jingle) pin; + if (jin.getAction().equals(Jingle.Action.SESSIONINITIATE)) { + return true; + } + } + } + } + return false; + } + }; + + jingleSessionRequestListeners = new ArrayList(); + + // Start a packet listener for session initiation requests + connection.addPacketListener(new PacketListener() { + public void processPacket(Packet packet) { + triggerSessionRequested((Jingle) packet); + } + }, initRequestFilter); + } + + /** + * Disconnect all Jingle Sessions + */ + public void disconnectAllSessions() { + + List sessions = jingleSessions.subList(0, jingleSessions.size()); + + for (JingleSession jingleSession : sessions) + try { + jingleSession.terminate(); + } catch (XMPPException e) { + e.printStackTrace(); + } + + sessions.clear(); + } + + /** + * Activates the listenerJingles on a Jingle session request. + * + * @param initJin the packet that must be passed to the jingleSessionRequestListener. + */ + void triggerSessionRequested(Jingle initJin) { + + JingleSessionRequestListener[] jingleSessionRequestListeners = null; + + // Make a synchronized copy of the listenerJingles + synchronized (this.jingleSessionRequestListeners) { + jingleSessionRequestListeners = new JingleSessionRequestListener[this.jingleSessionRequestListeners.size()]; + this.jingleSessionRequestListeners.toArray(jingleSessionRequestListeners); + } + + // ... and let them know of the event + JingleSessionRequest request = new JingleSessionRequest(this, initJin); + for (int i = 0; i < jingleSessionRequestListeners.length; i++) { + jingleSessionRequestListeners[i].sessionRequested(request); + } + } + + // Session creation + + /** + * Creates an Jingle session to start a communication with another user. + * + * @param responder the fully qualified jabber ID with resource of the other + * user. + * @param payloadTypes list of supported payload types + * @return The session on which the negotiation can be run. + */ + public OutgoingJingleSession createOutgoingJingleSession(String responder, + List payloadTypes) throws XMPPException { + + if (responder == null || StringUtils.parseName(responder).length() <= 0 + || StringUtils.parseServer(responder).length() <= 0 + || StringUtils.parseResource(responder).length() <= 0) { + throw new IllegalArgumentException( + "The provided user id was not fully qualified"); + } + + OutgoingJingleSession session; + + TransportResolver resolver = jingleTransportManager.getResolver(); + + if (jingleMediaManager != null) + session = new OutgoingJingleSession(connection, responder, payloadTypes, resolver, jingleMediaManager); + else + session = new OutgoingJingleSession(connection, responder, payloadTypes, jingleTransportManager.getResolver()); + + triggerSessionCreated(session); + + return session; + } + + /** + * Creates an Jingle session to start a communication with another user. + * + * @param responder the fully qualified jabber ID with resource of the other + * user. + * @return the session on which the negotiation can be run. + */ + public OutgoingJingleSession createOutgoingJingleSession(String responder) throws XMPPException { + if (this.getMediaManager() == null) return null; + return createOutgoingJingleSession(responder, this.getMediaManager().getPayloads()); + } + + /** + * When the session request is acceptable, this method should be invoked. It + * will create an JingleSession which allows the negotiation to procede. + * + * @param request the remote request that is being accepted. + * @param payloadTypes the list of supported Payload types that can be accepted + * @return the session which manages the rest of the negotiation. + */ + IncomingJingleSession createIncomingJingleSession( + JingleSessionRequest request, List payloadTypes) throws XMPPException { + if (request == null) { + throw new NullPointerException("Received request cannot be null"); + } + + IncomingJingleSession session; + + TransportResolver resolver = jingleTransportManager.getResolver(); + + if (jingleMediaManager != null) + session = new IncomingJingleSession(connection, request + .getFrom(), payloadTypes, resolver, jingleMediaManager); + else + session = new IncomingJingleSession(connection, request + .getFrom(), payloadTypes, resolver); + + triggerSessionCreated(session); + + return session; + } + + /** + * When the session request is acceptable, this method should be invoked. It + * will create an JingleSession which allows the negotiation to procede. + * This method use JingleMediaManager to select the supported Payload types. + * + * @param request the remote request that is being accepted. + * @return the session which manages the rest of the negotiation. + */ + IncomingJingleSession createIncomingJingleSession(JingleSessionRequest request) throws XMPPException { + if (request == null) { + throw new NullPointerException("JingleMediaManager is not defined"); + } + if (jingleMediaManager == null) return null; + return createIncomingJingleSession(request, jingleMediaManager.getPayloads()); + } + + /** + * Get a session with the informed JID. If no session is found, return null. + * + * @param jid + * @return + */ + public JingleSession getSession(String jid) { + for (JingleSession jingleSession : jingleSessions) { + if (jingleSession instanceof OutgoingJingleSession) { + if (jingleSession.getResponder().equals(jid)) { + return jingleSession; + } + } else if (jingleSession instanceof IncomingJingleSession) { + if (jingleSession.getInitiator().equals(jid)) { + return jingleSession; + } + } + } + return null; + } + + /** + * Reject the session. If we don't want to accept the new session, send an + * appropriate error packet. + * + * @param request the request to be rejected. + */ + protected void rejectIncomingJingleSession(JingleSessionRequest request) { + Jingle initiation = request.getJingle(); + + IQ rejection = JingleSession.createError(initiation.getPacketID(), initiation + .getFrom(), initiation.getTo(), 403, "Declined"); + connection.sendPacket(rejection); + } +} \ No newline at end of file diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleNegotiator.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleNegotiator.java new file mode 100644 index 000000000..ef5580d12 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleNegotiator.java @@ -0,0 +1,354 @@ +package org.jivesoftware.smackx.jingle; + +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.jingle.listeners.JingleListener; +import org.jivesoftware.smackx.packet.Jingle; +import org.jivesoftware.smackx.packet.JingleError; + +import java.util.ArrayList; + +/** + * Basic Jingle negotiator. + *

+ *

+ *

+ * JingleNegotiator implements some basic behavior for every Jingle negotiation. + * It implements a "state" pattern: each stage should process Jingle packets and + * act depending on the current state in the negotiation... + *

+ *

+ * + * @author Alvaro Saurin + */ +public abstract class JingleNegotiator { + + private State state; // Current negotiation state + + private XMPPConnection connection; // The connection associated + + private final ArrayList listeners = new ArrayList(); + + private String expectedAckId; + + /** + * Default constructor. + */ + public JingleNegotiator() { + this(null); + } + + /** + * Default constructor with a XMPPConnection + * + * @param connection the connection associated + */ + public JingleNegotiator(XMPPConnection connection) { + this.connection = connection; + state = null; + } + + /** + * Get the XMPP connection associated with this negotiation. + * + * @return the connection + */ + public XMPPConnection getConnection() { + return connection; + } + + /** + * Set the XMPP connection associated. + * + * @param connection the connection to set + */ + public void setConnection(XMPPConnection connection) { + this.connection = connection; + } + + /** + * Inform if current state is null + * + * @return true if current state is null + */ + public boolean invalidState() { + return state == null; + } + + /** + * Return the current state + * + * @return the state + */ + public State getState() { + return state; + } + + /** + * Return the current state class + * + * @return the state + */ + public Class getStateClass() { + if (state != null) { + return state.getClass(); + } else { + return Object.class; + } + } + + /** + * Set the new state. + * + * @param newState the state to set + * @throws XMPPException + */ + protected void setState(State newState) { + boolean transition = newState != state; + + if (transition && state != null) { + state.eventExit(); + } + + state = newState; + + if (transition && state != null) { + state.eventEnter(); + } + } + + // Acks management + + /** + * Add expected ID + * @param id + */ + public void addExpectedId(String id) { + expectedAckId = id; + } + + /** + * Check if the passed ID is the expected ID + * @param id + * @return + */ + public boolean isExpectedId(String id) { + if (id != null) { + return id.equals(expectedAckId); + } else { + return false; + } + } + + /** + * Remove and expected ID + * @param id + */ + public void removeExpectedId(String id) { + addExpectedId((String) null); + } + + // Listeners + + /** + * Add a Jingle session listener to listen to incoming session requests. + * + * @param li The listener + * @see org.jivesoftware.smackx.jingle.listeners.JingleListener + */ + public void addListener(JingleListener li) { + synchronized (listeners) { + listeners.add(li); + } + } + + /** + * Removes a Jingle session listener. + * + * @param li The jingle session listener to be removed + * @see org.jivesoftware.smackx.jingle.listeners.JingleListener + */ + public void removeListener(JingleListener li) { + synchronized (listeners) { + listeners.remove(li); + } + } + + /** + * Get a copy of the listeners + * + * @return a copy of the listeners + */ + protected ArrayList getListenersList() { + ArrayList result; + + synchronized (listeners) { + result = new ArrayList(listeners); + } + + return result; + } + + /** + * Dispatch an incomming packet. This method is responsible for recognizing + * the packet type and, depending on the current state, deliverying the + * packet to the right event handler and wait for a response. + * + * @param iq the packet received + * @param id the ID of the response that will be sent + * @return the new packet to send (either a Jingle or an IQ error). + * @throws XMPPException + */ + public abstract IQ dispatchIncomingPacket(IQ iq, String id) + throws XMPPException; + + /** + * Close the negotiation. + */ + public void close() { + setState(null); + } + + /** + * A Jingle exception. + * + * @author Alvaro Saurin + */ + public static class JingleException extends XMPPException { + + private final JingleError error; + + /** + * Default constructor. + */ + public JingleException() { + super(); + error = null; + } + + /** + * Constructor with an error message. + * + * @param msg The message. + */ + public JingleException(String msg) { + super(msg); + error = null; + } + + /** + * Constructor with an error response. + * + * @param error The error message. + */ + public JingleException(JingleError error) { + super(); + this.error = error; + } + + /** + * Return the error message. + * + * @return the error + */ + public JingleError getError() { + return error; + } + } + + /** + * Negotiation state and events. + *

+ *

+ *

+ * Describes the negotiation stage. + */ + public static class State { + + private JingleNegotiator neg; // The negotiator + + /** + * Default constructor, with a reference to the negotiator. + * + * @param neg The negotiator instance. + */ + public State(JingleNegotiator neg) { + this.neg = neg; + } + + /** + * Get the negotiator + * + * @return the negotiator. + */ + public JingleNegotiator getNegotiator() { + return neg; + } + + /** + * Set the negotiator. + * + * @param neg the neg to set + */ + public void setNegotiator(JingleNegotiator neg) { + this.neg = neg; + } + + // State transition events + + public Jingle eventAck(IQ iq) throws XMPPException { + // We have received an Ack + return null; + } + + public void eventError(IQ iq) throws XMPPException { + throw new JingleException(iq.getError().getMessage()); + } + + public Jingle eventInvite() throws XMPPException { + throw new IllegalStateException( + "Negotiation can not be started in this state."); + } + + public Jingle eventInitiate(Jingle jin) throws XMPPException { + return null; + } + + public Jingle eventAccept(Jingle jin) throws XMPPException { + return null; + } + + public Jingle eventRedirect(Jingle jin) throws XMPPException { + return null; + } + + public Jingle eventModify(Jingle jin) throws XMPPException { + return null; + } + + public Jingle eventDecline(Jingle jin) throws XMPPException { + return null; + } + + public Jingle eventInfo(Jingle jin) throws XMPPException { + return null; + } + + public Jingle eventTerminate(Jingle jin) throws XMPPException { + if (neg != null) { + neg.close(); + } + return null; + } + + public void eventEnter() { + } + + public void eventExit() { + if (neg != null) { + neg.removeExpectedId(null); + } + } + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSession.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSession.java new file mode 100644 index 000000000..fac9b94d3 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSession.java @@ -0,0 +1,1129 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright (C) 2002-2006 Jive Software. All rights reserved. + * ==================================================================== + * The Jive Software License (based on Apache Software License, Version 1.1) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by + * Jive Software (http://www.jivesoftware.com)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Smack" and "Jive Software" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please + * contact webmaster@jivesoftware.com. + * + * 5. Products derived from this software may not be called "Smack", + * nor may "Smack" appear in their name, without prior written + * permission of Jive Software. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL JIVE SOFTWARE OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + */ + +package org.jivesoftware.smackx.jingle; + +import org.jivesoftware.smack.ConnectionListener; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smackx.jingle.listeners.JingleListener; +import org.jivesoftware.smackx.jingle.listeners.JingleMediaListener; +import org.jivesoftware.smackx.jingle.listeners.JingleSessionListener; +import org.jivesoftware.smackx.jingle.listeners.JingleTransportListener; +import org.jivesoftware.smackx.jingle.media.*; +import org.jivesoftware.smackx.jingle.nat.TransportCandidate; +import org.jivesoftware.smackx.jingle.nat.TransportNegotiator; +import org.jivesoftware.smackx.packet.Jingle; +import org.jivesoftware.smackx.packet.JingleContentDescription; +import org.jivesoftware.smackx.packet.JingleContentDescription.JinglePayloadType; +import org.jivesoftware.smackx.packet.JingleContentInfo; +import org.jivesoftware.smackx.packet.JingleError; +import org.jivesoftware.smackx.packet.JingleTransport.JingleTransportCandidate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Random; + +/** + * An abstract Jingle session. + * + * This class contains some basic properties of every Jingle session. However, + * the concrete implementation can be found in subclasses. + * + * @author Alvaro Saurin + * @see IncomingJingleSession + * @see OutgoingJingleSession + */ +public abstract class JingleSession extends JingleNegotiator { + + // static + private static final HashMap sessions = new HashMap(); + + private static final Random randomGenerator = new Random(); + + // non-static + + private String initiator; // Who started the communication + + private String responder; // The other endpoint + + private String sid; // A unique id that identifies this session + + private MediaNegotiator mediaNeg; // The description... + + private TransportNegotiator transNeg; // and transport negotiators + + PacketListener packetListener; + + PacketFilter packetFilter; + + protected JingleMediaManager jingleMediaManager = null; + + protected JingleMediaSession jingleMediaSession = null; + + static int ccc=0; + + /** + * Full featured JingleSession constructor + * + * @param conn XMPPConnection + * @param initiator the initiator JID + * @param responder the responder JID + * @param sessionid the session ID + * @param jingleMediaManager the jingleMediaManager + */ + protected JingleSession(XMPPConnection conn, String initiator, String responder, + String sessionid, JingleMediaManager jingleMediaManager) { + super(conn); + + this.mediaNeg = null; + this.transNeg = null; + + this.initiator = initiator; + this.responder = responder; + this.sid = sessionid; + + this.jingleMediaManager = jingleMediaManager; + + // Add the session to the list and register the listeneres + registerInstance(); + installConnectionListeners(conn); + } + + /** + * JingleSession constructor + * + * @param conn XMPPConnection + * @param initiator the initiator JID + * @param responder the responder JID + */ + protected JingleSession(XMPPConnection conn, String initiator, String responder) { + this(conn, initiator, responder, null, null); + } + + /** + * JingleSession constructor + * + * @param conn XMPPConnection + * @param initiator the initiator JID + * @param responder the responder JID + * @param jingleMediaManager the jingleMediaManager + */ + protected JingleSession(XMPPConnection conn, String initiator, String responder, JingleMediaManager jingleMediaManager) { + this(conn, initiator, responder, null, jingleMediaManager); + } + + /** + * Get the session initiator + * + * @return the initiator + */ + public String getInitiator() { + return initiator; + } + + /** + * Set the session initiator + * + * @param initiator the initiator to set + */ + public void setInitiator(String initiator) { + this.initiator = initiator; + } + + /** + * Get the Media Manager of this Jingle Session + * + * @return + */ + public JingleMediaManager getMediaManager() { + return jingleMediaManager; + } + + /** + * Set the Media Manager of this Jingle Session + * + * @param jingleMediaManager + */ + public void setMediaManager(JingleMediaManager jingleMediaManager) { + this.jingleMediaManager = jingleMediaManager; + } + + /** + * Get the session responder + * + * @return the responder + */ + public String getResponder() { + return responder; + } + + /** + * Set the session responder. + * + * @param responder the receptor to set + */ + public void setResponder(String responder) { + this.responder = responder; + } + + /** + * Get the session ID + * + * @return the sid + */ + public String getSid() { + return sid; + } + + /** + * Set the session ID + * + * @param sessionId the sid to set + */ + protected void setSid(String sessionId) { + sid = sessionId; + } + + /** + * Generate a unique session ID. + */ + protected String generateSessionId() { + return String.valueOf(Math.abs(randomGenerator.nextLong())); + } + + /** + * Obtain the description negotiator for this session + * + * @return the description negotiator + */ + protected MediaNegotiator getMediaNeg() { + return mediaNeg; + } + + /** + * Set the jmf negotiator. + * + * @param mediaNeg the description negotiator to set + */ + protected void setMediaNeg(MediaNegotiator mediaNeg) { + destroyMediaNeg(); + this.mediaNeg = mediaNeg; + } + + /** + * Destroy the jmf negotiator. + */ + protected void destroyMediaNeg() { + if (mediaNeg != null) { + mediaNeg.close(); + mediaNeg = null; + } + } + + /** + * Obtain the transport negotiator for this session. + * + * @return the transport negotiator instance + */ + protected TransportNegotiator getTransportNeg() { + return transNeg; + } + + /** + * Set TransportNegociator + * + * @param transNeg the transNeg to set + */ + protected void setTransportNeg(TransportNegotiator transNeg) { + destroyTransportNeg(); + this.transNeg = transNeg; + } + + /** + * Destroy the transport negotiator. + */ + protected void destroyTransportNeg() { + if (transNeg != null) { + transNeg.close(); + transNeg = null; + } + } + + /** + * Return true if the transport and content negotiators have finished + */ + public boolean isFullyEstablished() { + if (!isValid()) { + return false; + } + if (!getTransportNeg().isFullyEstablished() + || !getMediaNeg().isFullyEstablished()) { + return false; + } + return true; + } + + /** + * Return true if the session is valid (ie, it has all the required + * elements initialized). + * + * @return true if the session is valid. + */ + public boolean isValid() { + return mediaNeg != null && transNeg != null && sid != null && initiator != null; + } + + /** + * Dispatch an incoming packet. The medthod is responsible for recognizing + * the packet type and, depending on the current state, deliverying the + * packet to the right event handler and wait for a response. + * + * @param iq the packet received + * @return the new Jingle packet to send. + * @throws XMPPException + */ + public IQ dispatchIncomingPacket(IQ iq, String id) throws XMPPException { + IQ jout = null; + + if (invalidState()) { + throw new IllegalStateException( + "Illegal state in dispatch packet in Session manager."); + } else { + if (iq == null) { + // If there is no input packet, then we must be inviting... + jout = getState().eventInvite(); + } else { + if (iq.getType().equals(IQ.Type.ERROR)) { + // Process errors + getState().eventError(iq); + } else if (iq.getType().equals(IQ.Type.RESULT)) { + // Process ACKs + if (isExpectedId(iq.getPacketID())) { + jout = getState().eventAck(iq); + removeExpectedId(iq.getPacketID()); + } + } else if (iq instanceof Jingle) { + // It is not an error: it is a Jingle packet... + Jingle jin = (Jingle) iq; + Jingle.Action action = jin.getAction(); + + if (action != null) { + if (action.equals(Jingle.Action.SESSIONACCEPT)) { + jout = getState().eventAccept(jin); + } else if (action.equals(Jingle.Action.SESSIONINFO)) { + jout = getState().eventInfo(jin); + } else if (action.equals(Jingle.Action.SESSIONINITIATE)) { + jout = getState().eventInitiate(jin); + } else if (action.equals(Jingle.Action.SESSIONREDIRECT)) { + jout = getState().eventRedirect(jin); + } else if (action.equals(Jingle.Action.SESSIONTERMINATE)) { + jout = getState().eventTerminate(jin); + } + } else { + jout = errorMalformedStanza(iq); + } + } + } + + if (jout != null) { + // Save the packet id, for recognizing ACKs... + addExpectedId(jout.getPacketID()); + } + } + + return jout; + } + + /** + * Process and respond to an incomming packet. + *

+ * This method is called from the packet listener dispatcher when a new + * packet has arrived. The medthod is responsible for recognizing the packet + * type and, depending on the current state, deliverying it to the right + * event handler and wait for a response. The response will be another + * Jingle packet that will be sent to the other endpoint. + * + * @param iq the packet received + * @return the new Jingle packet to send. + * @throws XMPPException + */ + public synchronized IQ respond(IQ iq) throws XMPPException { + IQ response = null; + + if (isValid()) { + String responseId = null; + IQ sessionResponse = null; + IQ descriptionResponse = null; + IQ transportResponse = null; + + // Send the packet to the right event handler for the session... + try { + sessionResponse = dispatchIncomingPacket(iq, null); + if (sessionResponse != null) { + responseId = sessionResponse.getPacketID(); + } + + // ... and do the same for the Description and Transport + // parts... + if (mediaNeg != null) { + descriptionResponse = mediaNeg.dispatchIncomingPacket(iq, responseId); + } + + if (transNeg != null) { + transportResponse = transNeg.dispatchIncomingPacket(iq, responseId); + } + + // Acknowledge the IQ reception + sendAck(iq); + + // ... and send all these parts in a Jingle response. + response = sendJingleParts(iq, (Jingle) sessionResponse, + (Jingle) descriptionResponse, (Jingle) transportResponse); + + } + catch (JingleException e) { + // Send an error message, if present + JingleError error = e.getError(); + if (error != null) { + sendFormattedError(iq, error); + } + + // Notify the session end and close everything... + triggerSessionClosedOnError(e); + close(); + } + } + + return response; + } + + // Packet formatting and delivery + + /** + * Put together all the parts ina Jingle packet. + * + * @return the new Jingle packet + */ + private Jingle sendJingleParts(IQ iq, Jingle jSes, Jingle jDesc, + Jingle jTrans) { + Jingle response = null; + + if (jSes != null) { + jSes.addDescriptions(jDesc.getDescriptionsList()); + jSes.addTransports(jTrans.getTransportsList()); + + response = sendFormattedJingle(iq, jSes); + } else { + // If we don't have a valid session message, then we must send + // separated messages for transport and jmf... + if (jDesc != null) { + response = sendFormattedJingle(iq, jDesc); + } + + if (jTrans != null) { + response = sendFormattedJingle(iq, jTrans); + } + } + + return response; + } + + /** + * Complete and send an error. Complete all the null fields in an IQ error + * reponse, using the sesssion information we have or some info from the + * incoming packet. + * + * @param iq The Jingle packet we are responing to + * @param error the IQ packet we want to complete and send + */ + protected IQ sendFormattedError(IQ iq, JingleError error) { + IQ perror = null; + if (error != null) { + perror = createIQ(getSid(), iq.getFrom(), iq.getTo(), IQ.Type.ERROR); + + // Fill in the fields with the info from the Jingle packet + perror.setPacketID(iq.getPacketID()); + perror.addExtension(error); + + getConnection().sendPacket(perror); + } + return perror; + } + + /** + * Complete and send a packet. Complete all the null fields in a Jingle + * reponse, using the session information we have or some info from the + * incoming packet. + * + * @param iq The Jingle packet we are responing to + * @param jout the Jingle packet we want to complete and send + */ + public Jingle sendFormattedJingle(IQ iq, Jingle jout) { + if (jout != null) { + if (jout.getInitiator() == null) { + jout.setInitiator(getInitiator()); + } + + if (jout.getResponder() == null) { + jout.setResponder(getResponder()); + } + + if (jout.getSid() == null) { + jout.setSid(getSid()); + } + + String me = getConnection().getUser(); + String other = getResponder().equals(me) ? getInitiator() : getResponder(); + + if (jout.getTo() == null) { + if (iq != null) { + jout.setTo(iq.getFrom()); + } else { + jout.setTo(other); + } + } + + if (jout.getFrom() == null) { + if (iq != null) { + jout.setFrom(iq.getTo()); + } else { + jout.setFrom(me); + } + } + getConnection().sendPacket(jout); + } + return jout; + } + + /** + * Complete and send a packet. Complete all the null fields in a Jingle + * reponse, using the session information we have. + * + * @param jout the Jingle packet we want to complete and send + */ + public Jingle sendFormattedJingle(Jingle jout) { + return sendFormattedJingle(null, jout); + } + + /** + * Send an error indicating that the stanza is malformed. + * + * @param iq + */ + protected IQ errorMalformedStanza(IQ iq) { + // FIXME: implement with the right message... + return createError(iq.getPacketID(), iq.getFrom(), getConnection().getUser(), + 400, "Bad Request"); + } + + /** + * Check if we have an established session and, in that case, send an Accept + * packet. + */ + protected Jingle sendAcceptIfFullyEstablished() { + Jingle result = null; + if (isFullyEstablished()) { + // Ok, send a packet saying that we accept this session + Jingle jout = new Jingle(Jingle.Action.SESSIONACCEPT); + jout.setType(IQ.Type.SET); + + result = sendFormattedJingle(jout); + } + return result; + } + + /** + * Acknowledge a IQ packet. + * + * @param iq The IQ to acknowledge + */ + private IQ sendAck(IQ iq) { + IQ result = null; + + if (iq != null) { + // Don't acknowledge ACKs, errors... + if (iq.getType().equals(IQ.Type.SET)) { + IQ ack = createIQ(iq.getPacketID(), iq.getFrom(), iq.getTo(), + IQ.Type.RESULT); + + getConnection().sendPacket(ack); + result = ack; + } + } + return result; + } + + /** + * Send a content info message. + */ + public synchronized void sendContentInfo(ContentInfo ci) { + if (isValid()) { + sendFormattedJingle(new Jingle(new JingleContentInfo(ci))); + } + } + + /** + * Get the content description the other part has accepted. + * + * @param jin The Jingle packet where they have accepted the session. + * @return The audio PayloadType they have accepted. + * @throws XMPPException + */ + protected PayloadType.Audio getAcceptedAudioPayloadType(Jingle jin) + throws XMPPException { + PayloadType.Audio acceptedPayloadType = null; + ArrayList jda = jin.getDescriptionsList(); + + if (jin.getAction().equals(Jingle.Action.SESSIONACCEPT)) { + + if (jda.size() > 1) { + throw new XMPPException( + "Unsupported feature: the number of accepted content descriptions is greater than 1."); + } else if (jda.size() == 1) { + JingleContentDescription jd = (JingleContentDescription) jda.get(0); + if (jd.getJinglePayloadTypesCount() > 1) { + throw new XMPPException( + "Unsupported feature: the number of accepted payload types is greater than 1."); + } + if (jd.getJinglePayloadTypesCount() == 1) { + JinglePayloadType jpt = (JinglePayloadType) jd + .getJinglePayloadTypesList().get(0); + acceptedPayloadType = (PayloadType.Audio) jpt.getPayloadType(); + } + } + } + return acceptedPayloadType; + } + + /** + * Get the accepted local candidate we have previously offered. + * + * @param jin The jingle packet where they accept the session + * @return The transport candidate they have accepted. + * @throws XMPPException + */ + protected TransportCandidate getAcceptedLocalCandidate(Jingle jin) + throws XMPPException { + ArrayList jta = jin.getTransportsList(); + TransportCandidate acceptedLocalCandidate = null; + + if (jin.getAction().equals(Jingle.Action.SESSIONACCEPT)) { + if (jta.size() > 1) { + throw new XMPPException( + "Unsupported feature: the number of accepted transports is greater than 1."); + } else if (jta.size() == 1) { + org.jivesoftware.smackx.packet.JingleTransport jt = (org.jivesoftware.smackx.packet.JingleTransport) jta.get(0); + + if (jt.getCandidatesCount() > 1) { + throw new XMPPException( + "Unsupported feature: the number of accepted transport candidates is greater than 1."); + } else if (jt.getCandidatesCount() == 1) { + JingleTransportCandidate jtc = (JingleTransportCandidate) jt + .getCandidatesList().get(0); + acceptedLocalCandidate = jtc.getMediaTransport(); + } + } + } + + return acceptedLocalCandidate; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + public int hashCode() { + return Jingle.getSessionHash(getSid(), getInitiator()); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + System.out.println("NULL"); + return false; + } + if (getClass() != obj.getClass()) { + System.out.println("CLASS DIFF"); + return false; + } + + final JingleSession other = (JingleSession) obj; + + if (initiator == null) { + if (other.initiator != null) { + return false; + } + } else if (!initiator.equals(other.initiator)) { + //Todo check behavior + // return false; + } + + if (responder == null) { + if (other.responder != null) { + return false; + } + } else if (!responder.equals(other.responder)) { + return false; + } + + if (sid == null) { + if (other.sid != null) { + return false; + } + } else if (!sid.equals(other.sid)) { + return false; + } + + return true; + } + + // Instances management + + /** + * Clean a session from the list. + * + * @param connection The connection to clean up + */ + private void unregisterInstanceFor(XMPPConnection connection) { + synchronized (sessions) { + sessions.remove(connection); + } + } + + /** + * Register this instance. + */ + private void registerInstance() { + synchronized (sessions) { + sessions.put(getConnection(), this); + } + } + + /** + * Returns the JingleSession related to a particular connection. + * + * @param con A XMPP connection + * @return a Jingle session + */ + public static JingleSession getInstanceFor(XMPPConnection con) { + if (con == null) { + throw new IllegalArgumentException("Connection cannot be null"); + } + + JingleSession result = null; + synchronized (sessions) { + if (sessions.containsKey(con)) { + result = (JingleSession) sessions.get(con); + } + } + + return result; + } + + + /** + * Configure a session, setting some action listeners... + * + * @param connection The connection to set up + */ + private void installConnectionListeners(final XMPPConnection connection) { + if (connection != null) { + connection.addConnectionListener(new ConnectionListener() { + public void connectionClosed() { + unregisterInstanceFor(connection); + } + + public void connectionClosedOnError(java.lang.Exception e) { + unregisterInstanceFor(connection); + } + + public void reconnectingIn(int i) { + } + + public void reconnectionSuccessful() { + } + + public void reconnectionFailed(Exception exception) { + } + }); + } + } + + /** + * Remove the packet listener used for processing packet. + */ + protected void removePacketListener() { + if (packetListener != null) { + getConnection().removePacketListener(packetListener); + System.out.println("REMOVE PACKET LISTENER"); + } + } + + /** + * Install the packet listener. The listener is responsible for responding + * to any packet that we receive... + */ + protected void updatePacketListener() { + removePacketListener(); + + System.out.println("UpdatePacketListener"); + + packetListener = new PacketListener() { + public void processPacket(Packet packet) { + try { + respond((IQ) packet); + } + catch (XMPPException e) { + e.printStackTrace(); + } + } + }; + + packetFilter = new PacketFilter() { + public boolean accept(Packet packet) { + + final int cc = ccc++; + System.out.println("filter:"+cc); + + if (packet instanceof IQ) { + IQ iq = (IQ) packet; + + String me = getConnection().getUser(); + + if (!iq.getTo().equals(me)) { + return false; + } + + String other = getResponder().equals(me) ? getInitiator() + : getResponder(); + + if (iq.getFrom() == null || !iq.getFrom().equals(other == null ? "" : other)) { + return false; + } + + if (iq instanceof Jingle) { + Jingle jin = (Jingle) iq; + + //System.out.println("Jingle: " + iq.toXML()); + + String sid = jin.getSid(); + if (!sid.equals(getSid())) { + System.out.println("Ignored Jingle(SID) " + sid + "|" + getSid() + " :" + iq.toXML()); + return false; + } + String ini = jin.getInitiator(); + if (!ini.equals(getInitiator())) { + System.out.println("Ignored Jingle(INI): " + iq.toXML()); + return false; + } + } else { + // We accept some non-Jingle IQ packets: ERRORs and ACKs + if (iq.getType().equals(IQ.Type.SET)) { + System.out.println("Ignored Jingle(TYPE): " + iq.toXML()); + return false; + } else if (iq.getType().equals(IQ.Type.GET)) { + System.out.println("Ignored Jingle(TYPE): " + iq.toXML()); + return false; + } + } + return true; + } + return false; + } + }; + + getConnection().addPacketListener(packetListener, packetFilter); + System.out.println("updatePacketListener"); + } + + // Listeners + + /** + * Add a listener for jmf negotiation events + * + * @param li The listener + */ + public void addMediaListener(JingleMediaListener li) { + if (getMediaNeg() != null) { + getMediaNeg().addListener(li); + } + } + + /** + * Remove a listener for jmf negotiation events + * + * @param li The listener + */ + public void removeMediaListener(JingleMediaListener li) { + if (getMediaNeg() != null) { + getMediaNeg().removeListener(li); + } + } + + /** + * Add a listener for transport negotiation events + * + * @param li The listener + */ + public void addTransportListener(JingleTransportListener li) { + if (getTransportNeg() != null) { + getTransportNeg().addListener(li); + } + } + + /** + * Remove a listener for transport negotiation events + * + * @param li The listener + */ + public void removeTransportListener(JingleTransportListener li) { + if (getTransportNeg() != null) { + getTransportNeg().removeListener(li); + } + } + + // Triggers + + /** + * Trigger a session closed event. + */ + protected void triggerSessionClosed(String reason) { + ArrayList listeners = getListenersList(); + Iterator iter = listeners.iterator(); + while (iter.hasNext()) { + JingleListener li = (JingleListener) iter.next(); + if (li instanceof JingleSessionListener) { + JingleSessionListener sli = (JingleSessionListener) li; + sli.sessionClosed(reason, this); + } + } + if (jingleMediaSession != null) { + jingleMediaSession.stopTrasmit(); + jingleMediaSession.stopReceive(); + } + } + + /** + * Trigger a session closed event due to an error. + */ + protected void triggerSessionClosedOnError(XMPPException exc) { + ArrayList listeners = getListenersList(); + Iterator iter = listeners.iterator(); + while (iter.hasNext()) { + JingleListener li = (JingleListener) iter.next(); + if (li instanceof JingleSessionListener) { + JingleSessionListener sli = (JingleSessionListener) li; + sli.sessionClosedOnError(exc,this); + } + } + if (jingleMediaSession != null) { + jingleMediaSession.stopTrasmit(); + jingleMediaSession.stopReceive(); + } + } + + /** + * Trigger a session established event. + */ + protected void triggerSessionEstablished(PayloadType pt, + TransportCandidate rc, TransportCandidate lc) { + ArrayList listeners = getListenersList(); + Iterator iter = listeners.iterator(); + while (iter.hasNext()) { + JingleListener li = (JingleListener) iter.next(); + if (li instanceof JingleSessionListener) { + JingleSessionListener sli = (JingleSessionListener) li; + sli.sessionEstablished(pt, rc, lc,this); + } + } + if (jingleMediaManager != null) { + jingleMediaSession = jingleMediaManager.createMediaSession(pt, rc, lc); + if (jingleMediaSession != null) { + jingleMediaSession.startTrasmit(); + jingleMediaSession.startReceive(); + } + } + + } + + /** + * Trigger a session redirect event. + */ + protected void triggerSessionRedirect(String arg) { + ArrayList listeners = getListenersList(); + Iterator iter = listeners.iterator(); + while (iter.hasNext()) { + JingleListener li = (JingleListener) iter.next(); + if (li instanceof JingleSessionListener) { + JingleSessionListener sli = (JingleSessionListener) li; + sli.sessionRedirected(arg,this); + } + } + } + + /** + * Trigger a session redirect event. + */ + protected void triggerSessionDeclined(String reason) { + ArrayList listeners = getListenersList(); + Iterator iter = listeners.iterator(); + while (iter.hasNext()) { + JingleListener li = (JingleListener) iter.next(); + if (li instanceof JingleSessionListener) { + JingleSessionListener sli = (JingleSessionListener) li; + sli.sessionDeclined(reason,this); + } + } + } + + /** + * Start the negotiation. + * + * @throws JingleException + * @throws XMPPException + */ + public abstract void start(JingleSessionRequest jin) throws XMPPException; + + /** + * Terminate the session. + * + * @throws XMPPException + */ + public void terminate() throws XMPPException { + System.out.println("State: " + this.getState()); + if (!invalidState()) { + Jingle result = null; + Jingle jout = new Jingle(Jingle.Action.SESSIONTERMINATE); + jout.setType(IQ.Type.SET); + sendFormattedJingle(jout); + triggerSessionClosed("Closed Locally"); + close(); + } else { + throw new IllegalStateException("Session Not Started"); + } + } + + /** + * Terminate negotiations. + */ + public void close() { + destroyMediaNeg(); + destroyTransportNeg(); + removePacketListener(); + System.out.println("Negociation Closed"); + super.close(); + } + + // Packet and error creation + + /** + * A convience method to create an IQ packet. + * + * @param ID The packet ID of the + * @param to To whom the packet is addressed. + * @param from From whom the packet is sent. + * @param type The iq type of the packet. + * @return The created IQ packet. + */ + public static IQ createIQ(String ID, String to, String from, + IQ.Type type) { + IQ iqPacket = new IQ() { + public String getChildElementXML() { + return null; + } + }; + + iqPacket.setPacketID(ID); + iqPacket.setTo(to); + iqPacket.setFrom(from); + iqPacket.setType(type); + + return iqPacket; + } + + /** + * A convience method to create an error packet. + * + * @param ID The packet ID of the + * @param to To whom the packet is addressed. + * @param from From whom the packet is sent. + * @param errCode The error code. + * @param errStr The error string. + * @return The created IQ packet. + */ + public static IQ createError(String ID, String to, String from, + int errCode, String errStr) { + + IQ iqError = createIQ(ID, to, from, IQ.Type.ERROR); + XMPPError error = new XMPPError(new XMPPError.Condition(errStr)); + iqError.setError(error); + + System.out.println(iqError.toXML()); + + return iqError; + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionRequest.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionRequest.java new file mode 100644 index 000000000..e1c3c6908 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionRequest.java @@ -0,0 +1,106 @@ +package org.jivesoftware.smackx.jingle; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.jingle.media.PayloadType; +import org.jivesoftware.smackx.packet.Jingle; + +import java.util.List; + +/** + * A Jingle session request. + * + * This class is a facade of a received Jingle request. The user can have direct + * access to the Jingle packet (JingleSessionRequest.getJingle() ) of + * the request or can use the convencience methods provided by this class. + * + * @author Alvaro Saurin + */ +public class JingleSessionRequest { + + private final Jingle jingle; // The Jingle packet + + private final JingleManager manager; // The manager associated to this + + // request + + /** + * A recieve request is constructed from the Jingle Initiation request + * received from the initator. + * + * @param manager The manager handling this request + * @param jingle The jingle IQ recieved from the initiator. + */ + public JingleSessionRequest(JingleManager manager, Jingle jingle) { + this.manager = manager; + this.jingle = jingle; + } + + /** + * Returns the fully-qualified jabber ID of the user that requested this + * session. + * + * @return Returns the fully-qualified jabber ID of the user that requested + * this session. + */ + public String getFrom() { + return jingle.getFrom(); + } + + /** + * Returns the session ID that uniquely identifies this session. + * + * @return Returns the session ID that uniquely identifies this session + */ + public String getSessionID() { + return jingle.getSid(); + } + + /** + * Returns the Jingle packet that was sent by the requestor which contains + * the parameters of the session. + */ + public Jingle getJingle() { + return jingle; + } + + /** + * Accepts this request and creates the incoming Jingle session. + * + * @param pts list of supported Payload Types + * @return Returns the IncomingJingleSession on which the + * negotiation can be carried out. + */ + public synchronized IncomingJingleSession accept(List pts) throws XMPPException { + IncomingJingleSession session = null; + synchronized (manager) { + session = manager.createIncomingJingleSession(this, + pts); + session.setInitialSessionRequest(this); + } + return session; + } + + /** + * Accepts this request and creates the incoming Jingle session. + * + * @return Returns the IncomingJingleSession on which the + * negotiation can be carried out. + */ + public synchronized IncomingJingleSession accept() throws XMPPException { + IncomingJingleSession session = null; + synchronized (manager) { + session = manager.createIncomingJingleSession(this); + session.setInitialSessionRequest(this); + } + return session; + } + + /** + * Rejects the session request. + */ + public synchronized void reject() { + synchronized (manager) { + manager.rejectIncomingJingleSession(this); + } + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/OutgoingJingleSession.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/OutgoingJingleSession.java new file mode 100644 index 000000000..44061aed1 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/OutgoingJingleSession.java @@ -0,0 +1,440 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright (C) 2002-2006 Jive Software. All rights reserved. + * ==================================================================== + * The Jive Software License (based on Apache Software License, Version 1.1) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by + * Jive Software (http://www.jivesoftware.com)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Smack" and "Jive Software" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please + * contact webmaster@jivesoftware.com. + * + * 5. Products derived from this software may not be called "Smack", + * nor may "Smack" appear in their name, without prior written + * permission of Jive Software. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL JIVE SOFTWARE OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + */ + +package org.jivesoftware.smackx.jingle; + +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.jingle.listeners.JingleMediaListener; +import org.jivesoftware.smackx.jingle.listeners.JingleTransportListener; +import org.jivesoftware.smackx.jingle.media.JingleMediaManager; +import org.jivesoftware.smackx.jingle.media.MediaNegotiator; +import org.jivesoftware.smackx.jingle.media.PayloadType; +import org.jivesoftware.smackx.jingle.nat.TransportCandidate; +import org.jivesoftware.smackx.jingle.nat.TransportNegotiator; +import org.jivesoftware.smackx.jingle.nat.TransportResolver; +import org.jivesoftware.smackx.packet.Jingle; +import org.jivesoftware.smackx.packet.JingleContentDescription; +import org.jivesoftware.smackx.packet.JingleContentDescription.JinglePayloadType; +import org.jivesoftware.smackx.packet.JingleError; + +import java.util.List; + +/** + * An outgoing Jingle session implementation. + * This class has especific bahavior to Request and establish a new Jingle Session. + * + * This class is not directly used by users. Instead, users should refer to the + * JingleManager class, that will create the appropiate instance... + * + * @author Alvaro Saurin + */ +public class OutgoingJingleSession extends JingleSession { + + // states + + private final Inviting inviting; + + private final Pending pending; + + private final Active active; + + /** + * Constructor for a Jingle outgoing session. + * + * @param conn the XMPP connection + * @param responder the other endpoint + * @param payloadTypes A list of payload types, in order of preference. + * @param resolver The transport resolver. + */ + protected OutgoingJingleSession(XMPPConnection conn, String responder, + List payloadTypes, TransportResolver resolver) { + + super(conn, conn.getUser(), responder); + + setSid(generateSessionId()); + + // Initialize the states. + inviting = new Inviting(this); + pending = new Pending(this); + active = new Active(this); + + // Create description and transport negotiatiors... + setMediaNeg(new MediaNegotiator(this, payloadTypes)); + if (resolver.getType().equals(TransportResolver.Type.rawupd)) { + setTransportNeg(new TransportNegotiator.RawUdp(this, resolver)); + } + if (resolver.getType().equals(TransportResolver.Type.ice)) { + setTransportNeg(new TransportNegotiator.Ice(this, resolver)); + } + } + + /** + * Constructor for a Jingle outgoing session with a defined Media Manager + * + * @param conn the XMPP connection + * @param responder the other endpoint + * @param payloadTypes A list of payload types, in order of preference. + * @param resolver The transport resolver. + * @param jingleMediaManager The Media Manager for this Session + */ + protected OutgoingJingleSession(XMPPConnection conn, String responder, + List payloadTypes, TransportResolver resolver, JingleMediaManager jingleMediaManager) { + this(conn, responder, payloadTypes, resolver); + this.jingleMediaManager = jingleMediaManager; + } + + /** + * Initiate the negotiation with an invitation. This method must be invoked + * for starting all negotiations. It is the initial starting point and, + * afterwards, any other packet processing is done with the packet listener + * callback... + * + * @throws IllegalStateException + */ + public void start(JingleSessionRequest req) throws IllegalStateException { + if (invalidState()) { + setState(inviting); + + // Use the standard behavior, using a null Jingle packet + try { + updatePacketListener(); + respond((Jingle) null); + } catch (XMPPException e) { + e.printStackTrace(); + close(); + } + } else { + throw new IllegalStateException("Starting session without null state."); + } + } + + /** + * Initiate the negotiation with an invitation. This method must be invoked + * for starting all negotiations. It is the initial starting point and, + * afterwards, any other packet processing is done with the packet listener + * callback... + * + * @throws IllegalStateException + */ + public void start() throws IllegalStateException { + start(null); + } + + // States + + /** + * Current state when we want to invite the other endpoint. + */ + public class Inviting extends JingleNegotiator.State { + + public Inviting(JingleNegotiator neg) { + super(neg); + } + + /** + * Create an invitation packet. + */ + public Jingle eventInvite() { + // Create an invitation packet, saving the Packet ID, for any ACK + return new Jingle(Jingle.Action.SESSIONINITIATE); + } + + /** + * The receiver has partially accepted our invitation. We go to the + * pending state while the content and transport negotiators work... + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ) + */ + public Jingle eventAck(IQ iq) { + setState(pending); + return null; + } + + /** + * The other endpoint has declined the invitation with an error. + * + * @throws XMPPException + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventError(org.jivesoftware.smack.packet.IQ) + */ + public void eventError(IQ iq) throws XMPPException { + triggerSessionDeclined(null); + super.eventError(iq); + } + + /** + * The other endpoint wants to redirect this connection. + */ + public Jingle eventRedirect(Jingle jin) { + String redirArg = null; + + // TODO: parse the redirection parameters... + + triggerSessionRedirect(redirArg); + return null; + } + } + + /** + * "Pending" state: we are waiting for the transport and content + * negotiators. + *

+ * Note: the transition from/to this state is done with listeners... + */ + public class Pending extends JingleNegotiator.State { + JingleMediaListener jingleMediaListener; + + JingleTransportListener jingleTransportListener; + + public Pending(JingleNegotiator neg) { + super(neg); + + // Create the listeners that will send a "session-accept" when + // the sub-negotiators are done. + jingleMediaListener = new JingleMediaListener() { + public void mediaClosed(PayloadType cand) { + } + + public void mediaEstablished(PayloadType pt) { + checkFullyEstablished(); + } + }; + + jingleTransportListener = new JingleTransportListener() { + public void transportEstablished(TransportCandidate local, + TransportCandidate remote) { + checkFullyEstablished(); + } + + public void transportClosed(TransportCandidate cand) { + } + + public void transportClosedOnError(XMPPException e) { + } + }; + } + + /** + * Enter in the pending state: install the listeners. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter() + */ + public void eventEnter() { + // Add the listeners to the sub-negotiators... + addMediaListener(jingleMediaListener); + addTransportListener(jingleTransportListener); + } + + /** + * Exit of the state: remove the listeners. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventExit() + */ + public void eventExit() { + removeMediaListener(jingleMediaListener); + removeTransportListener(jingleTransportListener); + } + + /** + * Check if the session has been fully accepted by all the + * sub-negotiators and, in that case, send an "accept" message... + */ + private void checkFullyEstablished() { + + if (isFullyEstablished()) { + + PayloadType.Audio bestCommonAudioPt = getMediaNeg() + .getBestCommonAudioPt(); + TransportCandidate bestRemoteCandidate = getTransportNeg() + .getBestRemoteCandidate(); + + // Ok, send a packet saying that we accept this session + // with the audio payload type and the transport + // candidate + Jingle jout = new Jingle(Jingle.Action.SESSIONACCEPT); + jout.addDescription(new JingleContentDescription.Audio( + new JinglePayloadType(bestCommonAudioPt))); + jout.addTransport(getTransportNeg().getJingleTransport( + bestRemoteCandidate)); + + // Send the "accept" and wait for the ACK + addExpectedId(jout.getPacketID()); + sendFormattedJingle(jout); + } + } + + /** + * The other endpoint has finally accepted our invitation. + * + * @throws XMPPException + */ + public Jingle eventAccept(Jingle jin) throws XMPPException { + + PayloadType acceptedPayloadType = null; + TransportCandidate acceptedLocalCandidate = null; + + // We process the "accepted" if we have finished the + // sub-negotiators. Maybe this is not needed (ie, the other endpoint + // can take the first valid transport candidate), but otherwise we + // must cancel the negotiators... + // + if (isFullyEstablished()) { + acceptedPayloadType = getAcceptedAudioPayloadType(jin); + acceptedLocalCandidate = getAcceptedLocalCandidate(jin); + + if (acceptedPayloadType != null && acceptedLocalCandidate != null) { + if (acceptedPayloadType.equals(getMediaNeg().getBestCommonAudioPt()) + && acceptedLocalCandidate.equals(getTransportNeg() + .getAcceptedLocalCandidate())) { + setState(active); + } + } else { + throw new JingleException(JingleError.NEGOTIATION_ERROR); + } + } + + return null; + } + + /** + * We have received the Ack of our "accept" + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ) + */ + public Jingle eventAck(IQ iq) { + setState(active); + return null; + } + + /** + * The other endpoint wants to redirect this connection. + */ + public Jingle eventRedirect(Jingle jin) { + String redirArg = null; + + // TODO: parse the redirection parameters... + + triggerSessionRedirect(redirArg); + return null; + } + + /** + * The other endpoint has rejected our invitation. + * + * @throws XMPPException + */ + public Jingle eventTerminate(Jingle jin) throws XMPPException { + triggerSessionDeclined(null); + return super.eventTerminate(jin); + } + + /** + * An error has occurred. + * + * @throws XMPPException + */ + public void eventError(IQ iq) throws XMPPException { + triggerSessionClosedOnError(new XMPPException(iq.getError().getMessage())); + super.eventError(iq); + } + } + + /** + * State when we have an established session. + */ + public class Active extends JingleNegotiator.State { + public Active(JingleNegotiator neg) { + super(neg); + } + + /** + * We have a established session: notify the listeners + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter() + */ + public void eventEnter() { + PayloadType.Audio bestCommonAudioPt = getMediaNeg().getBestCommonAudioPt(); + TransportCandidate bestRemoteCandidate = getTransportNeg() + .getBestRemoteCandidate(); + TransportCandidate acceptedLocalCandidate = getTransportNeg() + .getAcceptedLocalCandidate(); + + // Trigger the session established flag + System.out.println("eventEntered"); + triggerSessionEstablished(bestCommonAudioPt, bestRemoteCandidate, + acceptedLocalCandidate); + + super.eventEnter(); + } + + /** + * Terminate the connection. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventTerminate(org.jivesoftware.smackx.packet.Jingle) + */ + public Jingle eventTerminate(Jingle jin) throws XMPPException { + triggerSessionClosed("Closed Remotely"); + return super.eventTerminate(jin); + } + + /** + * An error has occurred. + * + * @throws XMPPException + */ + public void eventError(IQ iq) throws XMPPException { + triggerSessionClosedOnError(new XMPPException(iq.getError().getMessage())); + super.eventError(iq); + } + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/CreatedJingleSessionListener.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/CreatedJingleSessionListener.java new file mode 100644 index 000000000..961078527 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/CreatedJingleSessionListener.java @@ -0,0 +1,32 @@ +package org.jivesoftware.smackx.jingle.listeners; + +import org.jivesoftware.smackx.jingle.JingleSession; + +/** + * $RCSfile$ + * $Revision: $ + * $Date: 17/11/2006 + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Inteface used to dispatch a event when a Jingle session is created. + */ +public interface CreatedJingleSessionListener { + + public void sessionCreated(JingleSession jingleSession); + +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/JingleListener.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/JingleListener.java new file mode 100644 index 000000000..275fecfe4 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/JingleListener.java @@ -0,0 +1,40 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $11-07-2006 + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.jingle.listeners; + +/** + * Jingle listeners interface. + * + * This is the list of events that can be observed from a JingleSession and some + * sub negotiators. This listeners can be added to different elements of the + * Jingle model. + * + * For example, a JingleManager can notify any SessionRequestListenerListener + * listener when a new session request is received. In this case, the + * sessionRequested() of the listener will be executed, and the listener + * will be able to accept() or decline() the invitation. + * + * @author Alvaro Saurin + */ +public interface JingleListener { + + +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/JingleMediaInfoListener.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/JingleMediaInfoListener.java new file mode 100644 index 000000000..393011584 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/JingleMediaInfoListener.java @@ -0,0 +1,50 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $11-07-2006 + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.jingle.listeners; + +/** + * Interface for listening to jmf info events. + */ +public interface JingleMediaInfoListener extends JingleListener { + /** + * The other end is busy. + */ + public void mediaInfoBusy(); + + /** + * We are on hold. + */ + public void mediaInfoHold(); + + /** + * The jmf is muted. + */ + public void mediaInfoMute(); + + /** + * We are queued. + */ + public void mediaInfoQueued(); + + /** + * We are ringing. + */ + public void mediaInfoRinging(); +} \ No newline at end of file diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/JingleMediaListener.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/JingleMediaListener.java new file mode 100644 index 000000000..39efb35e0 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/JingleMediaListener.java @@ -0,0 +1,42 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $11-07-2006 + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.jingle.listeners; + +import org.jivesoftware.smackx.jingle.media.PayloadType; + +/** + * Interface for listening to jmf events. + */ +public interface JingleMediaListener extends JingleListener { + /** + * Notification that the jmf has been negotiated and established. + * + * @param pt The payload type agreed. + */ + public void mediaEstablished(PayloadType pt); + + /** + * Notification that a payload type must be cancelled + * + * @param cand The payload type that must be closed + */ + public void mediaClosed(PayloadType cand); +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/JingleSessionListener.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/JingleSessionListener.java new file mode 100644 index 000000000..f4aece244 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/JingleSessionListener.java @@ -0,0 +1,75 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $11-07-2006 + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.jingle.listeners; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.jingle.JingleSession; +import org.jivesoftware.smackx.jingle.media.PayloadType; +import org.jivesoftware.smackx.jingle.nat.TransportCandidate; + +/** + * Interface for listening for session events. + */ +public interface JingleSessionListener extends JingleListener { + /** + * Notification that the session has been established. Arguments specify + * the payload type and transport to use. + * + * @param pt the Payload tyep to use + * @param remoteCandidate the remote candidate to use for connecting to the remote + * service. + * @param localCandidate the local candidate where we must listen for connections + * @param jingleSession Session that called the method + */ + public void sessionEstablished(PayloadType pt, TransportCandidate remoteCandidate, + TransportCandidate localCandidate, JingleSession jingleSession); + + /** + * Notification that the session was declined. + * + * @param reason the reason (if any). + * @param jingleSession Session that called the method + */ + public void sessionDeclined(String reason, JingleSession jingleSession); + + /** + * Notification that the session was redirected. + * + * @param redirection + * @param jingleSession session that called the method + */ + public void sessionRedirected(String redirection, JingleSession jingleSession); + + /** + * Notification that the session was closed normally. + * + * @param reason the reason (if any). + * @param jingleSession Session that called the method + */ + public void sessionClosed(String reason, JingleSession jingleSession); + + /** + * Notification that the session was closed due to an exception. + * + * @param e the exception. + * @param jingleSession session that called the method + */ + public void sessionClosedOnError(XMPPException e, JingleSession jingleSession); +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/JingleSessionRequestListener.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/JingleSessionRequestListener.java new file mode 100644 index 000000000..6fb64a470 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/JingleSessionRequestListener.java @@ -0,0 +1,36 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $11-07-2006 + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.jingle.listeners; + +import org.jivesoftware.smackx.jingle.JingleSessionRequest; + +/** + * Interface to listener Jingle session requests. + * + * @author Alvaro Saurin + */ +public interface JingleSessionRequestListener extends JingleListener { + /** + * A request to start a session has been recieved from another user. + * + * @param request The request from the other user. + */ + public void sessionRequested(JingleSessionRequest request); +} \ No newline at end of file diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/JingleTransportListener.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/JingleTransportListener.java new file mode 100644 index 000000000..15239bb57 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/listeners/JingleTransportListener.java @@ -0,0 +1,56 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $11-07-2006 + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.jingle.listeners; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.jingle.nat.TransportCandidate; + +/** + * Interface for listening to transport events. + */ +public interface JingleTransportListener extends JingleListener { + + /** + * Notification that the transport has been established. + * + * @param local The transport candidate that has been used for listening + * in the local machine + * @param remote The transport candidate that has been used for + * transmitting to the remote machine + */ + public void transportEstablished(TransportCandidate local, + TransportCandidate remote); + + /** + * Notification that a transport must be cancelled. + * + * @param cand The transport candidate that must be cancelled. A value + * of "null" means all the transports for this session. + */ + public void transportClosed(TransportCandidate cand); + + /** + * Notification that the transport was closed due to an exception. + * + * @param e the exception. + */ + public void transportClosedOnError(XMPPException e); +} + diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/media/ContentInfo.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/media/ContentInfo.java new file mode 100644 index 000000000..3ecbc315f --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/media/ContentInfo.java @@ -0,0 +1,77 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $11-07-2006 + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.jingle.media; + +/** + * Content info. Content info messages are complementary messages that can be + * transmitted for informing of events like "busy", "ringtone", etc. + * + * @author Alvaro Saurin + */ +public abstract class ContentInfo { + + /** + * Audio content info messages. + * + * @author Alvaro Saurin + */ + public static class Audio extends ContentInfo { + + public static final ContentInfo.Audio BUSY = new ContentInfo.Audio("busy"); + + public static final ContentInfo.Audio HOLD = new ContentInfo.Audio("hold"); + + public static final ContentInfo.Audio MUTE = new ContentInfo.Audio("mute"); + + public static final ContentInfo.Audio QUEUED = new ContentInfo.Audio("queued"); + + public static final ContentInfo.Audio RINGING = new ContentInfo.Audio("ringing"); + + private String value; + + public Audio(String value) { + this.value = value; + } + + public String toString() { + return value; + } + + /** + * Returns the MediaInfo constant associated with the String value. + */ + public static ContentInfo fromString(String value) { + value = value.toLowerCase(); + if (value.equals("busy")) { + return BUSY; + } else if (value.equals("hold")) { + return HOLD; + } else if (value.equals("mute")) { + return MUTE; + } else if (value.equals("queued")) { + return QUEUED; + } else if (value.equals("ringing")) { + return RINGING; + } else { + return null; + } + } + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/media/JingleMediaManager.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/media/JingleMediaManager.java new file mode 100644 index 000000000..59ac842f6 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/media/JingleMediaManager.java @@ -0,0 +1,85 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $11-07-2006 + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.jingle.media; + +import org.jivesoftware.smackx.jingle.nat.TransportCandidate; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class provides necessary Jingle Session jmf methods and behavior. + * + * The goal of this class is to provide a flexible way to make JingleManager control jmf streaming APIs without implement them. + * For instance you can implement a file transfer using java sockets or a VOIP Media Manager using JMF. + * You can implement many JingleMediaManager according to you necessity. + * + * @author Thiago Camargo + */ +public abstract class JingleMediaManager { + + private List payloads = new ArrayList(); + + /** + * Return all supported Payloads for this Manager + * + * @return The Payload List + */ + public List getPayloads() { + return payloads; + } + + /** + * Adds a supported Payload type to Manager + * + * @param payloadType + */ + public void addPayloadType(PayloadType payloadType) { + payloads.add(payloadType); + } + + /** + * Removes a supported Payload type from Manager + * + * @param payloadType + */ + public void removePayloadType(PayloadType payloadType) { + payloads.remove(payloadType); + } + + /** + * Get the preferred Payload Type + */ + public PayloadType getPreferredPayloadType() { + //TODO a better way to choose the preferred Payload + return payloads.size() > 0 ? payloads.get(0) : null; + } + + /** + * Create a Media Session Implementation + * + * @param payloadType + * @param remote + * @param local + * @return + */ + public abstract JingleMediaSession createMediaSession(PayloadType payloadType, final TransportCandidate remote, final TransportCandidate local); +} \ No newline at end of file diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/media/JingleMediaSession.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/media/JingleMediaSession.java new file mode 100644 index 000000000..0af97853f --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/media/JingleMediaSession.java @@ -0,0 +1,119 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $11-07-2006 + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.jingle.media; + +import org.jivesoftware.smackx.jingle.nat.TransportCandidate; + +/** + * Public Abstract Class provides a clear interface between Media Session and Jingle API. + * + * When a Jingle Session is fully stablished, we will have a Payload Type and two transport candidates defined for it. + * Smack Jingle API donīt implement Media Transmit and Receive methods. + * But provides an interface to let the user implements it using another API. For instance: JMF. + * + * The Class that implements this one, must have the support to transmit and receive the jmf. + * This interface let the user choose his own jmf API. + * + * @author Thiago Camargo + */ +public abstract class JingleMediaSession { + + // Payload Type of the Session + private PayloadType payloadType; + // Local Transport details + private TransportCandidate local; + // Remote Transport details + private TransportCandidate remote; + + /** + * Creates a new JingleMediaSession Instance to handle Media methods. + * + * @param payloadType Payload Type of the transmittion + * @param remote Remote accepted Transport Candidate + * @param local Local accepted Transport Candidate + */ + public JingleMediaSession(PayloadType payloadType, TransportCandidate remote, + TransportCandidate local) { + this.local = local; + this.remote = remote; + this.payloadType = payloadType; + initialize(); + } + + /** + * Returns the PayloadType of the Media Session + * + * @return + */ + public PayloadType getPayloadType() { + return payloadType; + } + + /** + * Returns the Media Session local Candidate + * + * @return + */ + public TransportCandidate getLocal() { + return local; + } + + /** + * Returns the Media Session remote Candidate + * + * @return + */ + public TransportCandidate getRemote() { + return remote; + } + + /** + * Initialize the RTP Channel preparing to transmit and receive. + */ + public abstract void initialize(); + + /** + * Starts a RTP / UDP / TCP Transmission to the remote Candidate + */ + public abstract void startTrasmit(); + + /** + * Starts a RTP / UDP / TCP Receiver from the remote Candidate to local Candidate + */ + public abstract void startReceive(); + + /** + * Set transmit activity. If the active is true, the instance should trasmit. + * If it is set to false, the instance should pause transmit. + * @param active + */ + public abstract void setTrasmit(boolean active); + + /** + * Stops a RTP / UDP / TCP Transmission to the remote Candidate + */ + public abstract void stopTrasmit(); + + /** + * Stops a RTP / UDP / TCP Receiver from the remote Candidate to local Candidate + */ + public abstract void stopReceive(); + +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/media/MediaNegotiator.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/media/MediaNegotiator.java new file mode 100644 index 000000000..8cd234a5e --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/media/MediaNegotiator.java @@ -0,0 +1,555 @@ +package org.jivesoftware.smackx.jingle.media; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.jingle.JingleNegotiator; +import org.jivesoftware.smackx.jingle.JingleSession; +import org.jivesoftware.smackx.jingle.listeners.JingleListener; +import org.jivesoftware.smackx.jingle.listeners.JingleMediaListener; +import org.jivesoftware.smackx.packet.Jingle; +import org.jivesoftware.smackx.packet.JingleContentDescription; +import org.jivesoftware.smackx.packet.JingleContentDescription.JinglePayloadType; +import org.jivesoftware.smackx.packet.JingleError; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Manager for jmf descriptor negotiation. + * + * + * This class is responsible for managing the descriptor negotiation process, + * handling all the xmpp packets interchange and the stage control. + * + * @author Alvaro Saurin + */ +public class MediaNegotiator extends JingleNegotiator { + + private final JingleSession session; // The session this negotiation + + // Local and remote payload types... + + private final List localAudioPts = new ArrayList(); + + private final List remoteAudioPts = new ArrayList(); + + private PayloadType.Audio bestCommonAudioPt; + + // states + + private final Inviting inviting; + + private final Accepting accepting; + + private final Pending pending; + + private final Active active; + + /** + * Default constructor. The constructor establishes some basic parameters, + * but it does not start the negotiation. For starting the negotiation, call + * startNegotiation. + * + * @param js The jingle session. + */ + public MediaNegotiator(JingleSession js, List pts) { + super(js.getConnection()); + + session = js; + + bestCommonAudioPt = null; + + if (pts != null) { + if (pts.size() > 0) { + localAudioPts.addAll(pts); + } + } + + // Create the states... + inviting = new Inviting(this); + accepting = new Accepting(this); + pending = new Pending(this); + active = new Active(this); + } + + /** + * Dispatch an incomming packet. The medthod is responsible for recognizing + * the packet type and, depending on the current state, deliverying the + * packet to the right event handler and wait for a response. + * + * @param iq the packet received + * @return the new Jingle packet to send. + * @throws XMPPException + */ + public IQ dispatchIncomingPacket(IQ iq, String id) throws XMPPException { + IQ jout = null; + + if (invalidState()) { + if (iq == null) { + // With a null packet, we are just inviting the other end... + setState(inviting); + jout = getState().eventInvite(); + } else { + if (iq instanceof Jingle) { + // If there is no specific jmf action associated, then we + // are being invited to a new session... + setState(accepting); + jout = getState().eventInitiate((Jingle) iq); + } else { + throw new IllegalStateException( + "Invitation IQ received is not a Jingle packet in Media negotiator."); + } + } + } else { + if (iq == null) { + return null; + } else { + if (iq.getType().equals(IQ.Type.ERROR)) { + // Process errors + getState().eventError(iq); + } else if (iq.getType().equals(IQ.Type.RESULT)) { + // Process ACKs + if (isExpectedId(iq.getPacketID())) { + jout = getState().eventAck(iq); + removeExpectedId(iq.getPacketID()); + } + } else if (iq instanceof Jingle) { + // Get the action from the Jingle packet + Jingle jin = (Jingle) iq; + Jingle.Action action = jin.getAction(); + + if (action != null) { + if (action.equals(Jingle.Action.CONTENTACCEPT)) { + jout = getState().eventAccept(jin); + } else if (action.equals(Jingle.Action.CONTENTDECLINE)) { + jout = getState().eventDecline(jin); + } else if (action.equals(Jingle.Action.DESCRIPTIONINFO)) { + jout = getState().eventInfo(jin); + } else if (action.equals(Jingle.Action.CONTENTMODIFY)) { + jout = getState().eventModify(jin); + } + // Any unknown action will be ignored: it is not a msg + // to us... + } + } + } + } + + // Save the Id for any ACK + if (id != null) { + addExpectedId(id); + } else { + if (jout != null) { + addExpectedId(jout.getPacketID()); + } + } + + return jout; + } + + /** + * Return true if the content is negotiated. + * + * @return true if the content is negotiated. + */ + public boolean isEstablished() { + return getBestCommonAudioPt() != null; + } + + /** + * Return true if the content is fully negotiated. + * + * @return true if the content is fully negotiated. + */ + public boolean isFullyEstablished() { + return isEstablished() && getState() == active; + } + + // Payload types + + private PayloadType.Audio calculateBestCommonAudioPt(List remoteAudioPts) { + final ArrayList commonAudioPtsHere = new ArrayList(); + final ArrayList commonAudioPtsThere = new ArrayList(); + PayloadType.Audio result = null; + + if (!remoteAudioPts.isEmpty()) { + commonAudioPtsHere.addAll(localAudioPts); + commonAudioPtsHere.retainAll(remoteAudioPts); + + commonAudioPtsThere.addAll(remoteAudioPts); + commonAudioPtsThere.retainAll(localAudioPts); + + if (!commonAudioPtsHere.isEmpty() && !commonAudioPtsThere.isEmpty()) { + PayloadType.Audio bestPtHere = (PayloadType.Audio) commonAudioPtsHere + .get(0); + PayloadType.Audio bestPtThere = (PayloadType.Audio) commonAudioPtsThere + .get(0); + + // If both match, use it + if (bestPtHere.equals(bestPtThere)) { + result = bestPtHere; + } else { + // Otherwise, use the one of the initiator... + // FIXME: this is an invented behavior!!! + String initiator = session.getInitiator(); + String me = session.getConnection().getUser(); + + if (initiator.equals(me)) { + result = bestPtHere; + } else { + result = bestPtThere; + } + } + } + } + + return result; + } + + private List obtainPayloads(Jingle jin) { + List result = new ArrayList(); + Iterator iDescr = jin.getDescriptions(); + + // Add the list of payloads: iterate over the descriptions... + while (iDescr.hasNext()) { + JingleContentDescription.Audio descr = (JingleContentDescription.Audio) iDescr + .next(); + + if (descr != null) { + // ...and, then, over the payloads. + // Note: we use the last "description" in the packet... + result.clear(); + result.addAll(descr.getAudioPayloadTypesList()); + } + } + + return result; + } + + /** + * Adds a payload type to the list of remote payloads. + * + * @param pt the remote payload type + */ + public void addRemoteAudioPayloadType(PayloadType.Audio pt) { + if (pt != null) { + synchronized (remoteAudioPts) { + remoteAudioPts.add(pt); + } + } + } + + /** + * Create an offer for the list of audio payload types. + * + * @return a new Jingle packet with the list of audio Payload Types + */ + private Jingle getAudioPayloadTypesOffer() { + JingleContentDescription.Audio audioDescr = new JingleContentDescription.Audio(); + + // Add the list of payloads for audio and create a + // JingleContentDescription + // where we announce our payloads... + audioDescr.addAudioPayloadTypes(localAudioPts); + + return new Jingle(audioDescr); + } + + // Predefined messages and Errors + + /** + * Create an IQ "accept" message. + */ + private Jingle createAcceptMessage() { + Jingle jout = null; + + // If we hava a common best codec, send an accept right now... + jout = new Jingle(Jingle.Action.CONTENTACCEPT); + jout.addDescription(new JingleContentDescription.Audio( + new JinglePayloadType.Audio(bestCommonAudioPt))); + + return jout; + } + + // Payloads + + /** + * Get the best common codec between both parts. + * + * @return The best common PayloadType codec. + */ + public PayloadType.Audio getBestCommonAudioPt() { + return bestCommonAudioPt; + } + + // Events + + /** + * Trigger a session established event. + * + * @param bestPt payload type that has been agreed. + */ + protected void triggerMediaEstablished(PayloadType bestPt) { + ArrayList listeners = getListenersList(); + Iterator iter = listeners.iterator(); + while (iter.hasNext()) { + JingleListener li = (JingleListener) iter.next(); + if (li instanceof JingleMediaListener) { + JingleMediaListener mli = (JingleMediaListener) li; + mli.mediaEstablished(bestPt); + } + } + } + + /** + * Trigger a jmf closed event. + * + * @param currPt current payload type that is cancelled. + */ + protected void triggerMediaClosed(PayloadType currPt) { + ArrayList listeners = getListenersList(); + Iterator iter = listeners.iterator(); + while (iter.hasNext()) { + JingleListener li = (JingleListener) iter.next(); + if (li instanceof JingleMediaListener) { + JingleMediaListener mli = (JingleMediaListener) li; + mli.mediaClosed(currPt); + } + } + } + + /** + * Terminate the jmf negotiator + */ + public void close() { + super.close(); + } + + // States + + /** + * First stage when we send a session request. + */ + public class Inviting extends JingleNegotiator.State { + public Inviting(MediaNegotiator neg) { + super(neg); + } + + /** + * Create an initial Jingle packet, with the list of payload types that + * we support. The list is in order of preference. + */ + public Jingle eventInvite() { + return getAudioPayloadTypesOffer(); + } + + /** + * We have received the ACK for our invitation. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ) + */ + public Jingle eventAck(IQ iq) { + setState(pending); + return null; + } + } + + /** + * We are accepting connections. + */ + public class Accepting extends JingleNegotiator.State { + + public Accepting(MediaNegotiator neg) { + super(neg); + } + + /** + * We have received an invitation! Respond with a list of our payload + * types... + */ + public Jingle eventInitiate(Jingle jin) { + synchronized (remoteAudioPts) { + remoteAudioPts.addAll(obtainPayloads(jin)); + } + + return getAudioPayloadTypesOffer(); + } + + /** + * Process the ACK of our list of codecs (our offer). + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ) + */ + public Jingle eventAck(IQ iq) throws XMPPException { + Jingle response = null; + + if (!remoteAudioPts.isEmpty()) { + // Calculate the best common codec + bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts); + + // and send an accept if we havee an agreement... + if (bestCommonAudioPt != null) { + response = createAcceptMessage(); + } else { + throw new JingleException(JingleError.NO_COMMON_PAYLOAD); + } + + setState(pending); + } + + return response; + } + } + + /** + * Pending class: we are waiting for the other enpoint, that must say if it + * accepts or not... + */ + public class Pending extends JingleNegotiator.State { + public Pending(MediaNegotiator neg) { + super(neg); + } + + /** + * A content info has been received. This is done for publishing the + * list of payload types... + * + * @param jin The input packet + * @return a Jingle packet + * @throws JingleException + */ + public Jingle eventInfo(Jingle jin) throws JingleException { + PayloadType.Audio oldBestCommonAudioPt = bestCommonAudioPt; + List offeredPayloads = new ArrayList(); + Jingle response = null; + boolean ptChange = false; + + offeredPayloads = obtainPayloads(jin); + if (!offeredPayloads.isEmpty()) { + + synchronized (remoteAudioPts) { + remoteAudioPts.clear(); + remoteAudioPts.addAll(offeredPayloads); + } + + // Calculate the best common codec + bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts); + if (bestCommonAudioPt != null) { + // and send an accept if we have an agreement... + ptChange = !bestCommonAudioPt.equals(oldBestCommonAudioPt); + if (oldBestCommonAudioPt == null || ptChange) { + response = createAcceptMessage(); + } + } else { + throw new JingleException(JingleError.NO_COMMON_PAYLOAD); + } + } + + // Parse the Jingle and get the payload accepted + return response; + } + + /** + * A jmf description has been accepted. In this case, we must save the + * accepted payload type and notify any listener... + * + * @param jin The input packet + * @return a Jingle packet + * @throws JingleException + */ + public Jingle eventAccept(Jingle jin) throws JingleException { + PayloadType.Audio agreedCommonAudioPt; + List offeredPayloads = new ArrayList(); + Jingle response = null; + + if (bestCommonAudioPt == null) { + // Update the best common audio PT + bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts); + response = createAcceptMessage(); + } + + offeredPayloads = obtainPayloads(jin); + if (!offeredPayloads.isEmpty()) { + if (offeredPayloads.size() == 1) { + agreedCommonAudioPt = (PayloadType.Audio) offeredPayloads.get(0); + if (bestCommonAudioPt != null) { + // If the accepted PT matches the best payload + // everything is fine + if (!agreedCommonAudioPt.equals(bestCommonAudioPt)) { + throw new JingleException(JingleError.NEGOTIATION_ERROR); + } + } + + } else if (offeredPayloads.size() > 1) { + throw new JingleException(JingleError.MALFORMED_STANZA); + } + } + + return response; + } + + /** + * The other part has declined the our codec... + * + * @throws JingleException + */ + public Jingle eventDecline(Jingle inJingle) throws JingleException { + triggerMediaClosed(getBestCommonAudioPt()); + throw new JingleException(); + } + + /* + * (non-Javadoc) + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventError(org.jivesoftware.smack.packet.IQ) + */ + public void eventError(IQ iq) throws XMPPException { + triggerMediaClosed(getBestCommonAudioPt()); + super.eventError(iq); + } + + /** + * ACK received. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ) + */ + public Jingle eventAck(IQ iq) { + if (isEstablished()) { + setState(active); + } + + return null; + } + } + + /** + * "Active" state: we have an agreement about the codec... + */ + public class Active extends JingleNegotiator.State { + + public Active(MediaNegotiator neg) { + super(neg); + } + + /** + * We have an agreement. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter() + */ + public void eventEnter() { + triggerMediaEstablished(getBestCommonAudioPt()); + super.eventEnter(); + } + + /** + * We are breaking the contract... + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventExit() + */ + public void eventExit() { + triggerMediaClosed(getBestCommonAudioPt()); + super.eventExit(); + } + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/media/PayloadType.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/media/PayloadType.java new file mode 100644 index 000000000..0bfa80142 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/media/PayloadType.java @@ -0,0 +1,289 @@ +package org.jivesoftware.smackx.jingle.media; + +/** + * Represents a payload type. + * + * @author Alvaro Saurin + */ +public class PayloadType { + + public static int MAX_FIXED_PT = 95; + + public static int INVALID_PT = 65535; + + private int id; + + private String name; + + private int channels; + + /** + * Constructor with Id, name and number of channels + * + * @param id The identifier + * @param name A name + * @param channels The number of channels + */ + public PayloadType(int id, final String name, int channels) { + super(); + this.id = id; + this.name = name; + this.channels = channels; + } + + /** + * Default constructor. + */ + public PayloadType() { + this(INVALID_PT, null, 1); + } + + /** + * Constructor with Id and name + * + * @param id The identification + * @param name A name + */ + public PayloadType(int id, String name) { + this(id, name, 1); + } + + /** + * Copy constructor + * + * @param pt The other payload type. + */ + public PayloadType(PayloadType pt) { + this(pt.getId(), pt.getName(), pt.getChannels()); + } + + /** + * Get the ID. + * + * @return the ID + */ + public int getId() { + return id; + } + + /** + * Set the ID. + * + * @param id ID + */ + public void setId(int id) { + this.id = id; + } + + /** + * Get the printable name. + * + * @return printable name for the payload type + */ + public String getName() { + return name; + } + + /** + * Set the printable name. + * + * @param name the printable name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Get the number of channels used by this payload type. + * + * @return the number of channels + */ + public int getChannels() { + return channels; + } + + /** + * Set the numer of channels for a payload type. + * + * @param channels The number of channels + */ + public void setChannels(int channels) { + this.channels = channels; + } + + /** + * Return true if the Payload type is not valid + * + * @return true if the payload type is invalid + */ + public boolean isNull() { + if (getId() == INVALID_PT) { + return true; + } else if (getName() == null) { + return true; + } + return false; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + public int hashCode() { + final int PRIME = 31; + int result = 1; + result = PRIME * result + getChannels(); + result = PRIME * result + getId(); + result = PRIME * result + (getName() == null ? 0 : getName().hashCode()); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final PayloadType other = (PayloadType) obj; + if (getChannels() != other.getChannels()) { + return false; + } + if (getId() != other.getId()) { + return false; + } + + // Compare names only for dynamic payload types + if (getId() > MAX_FIXED_PT) { + if (getName() == null) { + if (other.getName() != null) { + return false; + } + } else if (!getName().equals(other.getName())) { + return false; + } + } + + return true; + } + + /** + * Audio payload type. + */ + public static class Audio extends PayloadType { + private int clockRate; + + /** + * Constructor with all the attributes of an Audio payload type + * + * @param id The identifier + * @param name The name assigned to this payload type + * @param channels The number of channels + * @param rate The clock rate + */ + public Audio(int id, String name, int channels, int rate) { + super(id, name, channels); + clockRate = rate; + } + + /** + * Empty constructor. + */ + public Audio() { + super(); + clockRate = 0; + } + + /** + * Constructor with Id and name + * + * @param id the Id for the payload type + * @param name the name of the payload type + */ + public Audio(int id, String name) { + super(id, name); + clockRate = 0; + } + + /** + * Copy constructor + * + * @param pt the other payload type + */ + public Audio(PayloadType pt) { + super(pt); + clockRate = 0; + } + + /** + * Copy constructor + * + * @param pt the other payload type + */ + public Audio(PayloadType.Audio pt) { + super(pt); + clockRate = pt.getClockRate(); + } + + /** + * Get the sampling clockRate for a payload type + * + * @return The sampling clockRate + */ + public int getClockRate() { + return clockRate; + } + + /** + * Set tha sampling clockRate for a playload type. + * + * @param rate The sampling clockRate + */ + public void setClockRate(int rate) { + clockRate = rate; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + public int hashCode() { + final int PRIME = 31; + int result = super.hashCode(); + result = PRIME * result + getClockRate(); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Audio other = (Audio) obj; + if (getClockRate() != other.getClockRate()) { + return false; + } + return true; + } + } +} \ No newline at end of file diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/BasicResolver.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/BasicResolver.java new file mode 100644 index 000000000..bc4689662 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/BasicResolver.java @@ -0,0 +1,102 @@ +package org.jivesoftware.smackx.jingle.nat; + +import org.jivesoftware.smack.XMPPException; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Enumeration; + +/** + * Basic Resolver takes all IP addresses of the interfaces and uses the + * first non-loopback address. + * A very simple and easy to use resolver. + */ +public class BasicResolver extends TransportResolver { + + /** + * Constructor. + */ + public BasicResolver() { + super(); + } + + /** + * Resolve the IP address. + *

+ * The BasicResolver takes the IP addresses of the interfaces and uses the + * first non-loopback, non-linklocal and non-sitelocal address. + */ + public synchronized void resolve() throws XMPPException { + + setResolveInit(); + + clearCandidates(); + + Enumeration ifaces = null; + + try { + ifaces = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException e) { + e.printStackTrace(); + } + + while (ifaces.hasMoreElements()) { + + NetworkInterface iface = (NetworkInterface) ifaces.nextElement(); + Enumeration iaddresses = iface.getInetAddresses(); + + while (iaddresses.hasMoreElements()) { + InetAddress iaddress = (InetAddress) iaddresses.nextElement(); + if (!iaddress.isLoopbackAddress() && !iaddress.isLinkLocalAddress() && !iaddress.isSiteLocalAddress()) { + TransportCandidate tr = new TransportCandidate.Fixed(iaddress.getHostAddress() != null ? iaddress.getHostAddress() : iaddress.getHostName(), getFreePort()); + tr.setLocalIp(iaddress.getHostAddress() != null ? iaddress.getHostAddress() : iaddress.getHostName()); + addCandidate(tr); + setResolveEnd(); + return; + } + } + } + + try { + ifaces = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException e) { + e.printStackTrace(); + } + + while (ifaces.hasMoreElements()) { + + NetworkInterface iface = (NetworkInterface) ifaces.nextElement(); + Enumeration iaddresses = iface.getInetAddresses(); + + while (iaddresses.hasMoreElements()) { + InetAddress iaddress = (InetAddress) iaddresses.nextElement(); + if (!iaddress.isLoopbackAddress() && !iaddress.isLinkLocalAddress()) { + TransportCandidate tr = new TransportCandidate.Fixed(iaddress.getHostAddress() != null ? iaddress.getHostAddress() : iaddress.getHostName(), getFreePort()); + tr.setLocalIp(iaddress.getHostAddress() != null ? iaddress.getHostAddress() : iaddress.getHostName()); + addCandidate(tr); + setResolveEnd(); + return; + } + } + } + + try { + TransportCandidate tr = new TransportCandidate.Fixed(InetAddress.getLocalHost().getHostAddress() != null ? InetAddress.getLocalHost().getHostAddress() : InetAddress.getLocalHost().getHostName(), getFreePort()); + tr.setLocalIp(InetAddress.getLocalHost().getHostAddress() != null ? InetAddress.getLocalHost().getHostAddress() : InetAddress.getLocalHost().getHostName()); + addCandidate(tr); + } catch (Exception e) { + e.printStackTrace(); + } + setResolveEnd(); + + } + + public void initialize() throws XMPPException { + setInitialized(); + } + + public void cancel() throws XMPPException { + // Nothing to do here + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/BasicTransportManager.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/BasicTransportManager.java new file mode 100644 index 000000000..a88837ff2 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/BasicTransportManager.java @@ -0,0 +1,32 @@ +package org.jivesoftware.smackx.jingle.nat; + +/** + * $RCSfile$ + * $Revision: $ + * $Date: 15/11/2006 + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A Basic Jingle Transport Manager implementation. + * + */ +public class BasicTransportManager extends JingleTransportManager{ + + protected TransportResolver createResolver() { + return new BasicResolver(); + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/BridgedResolver.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/BridgedResolver.java new file mode 100644 index 000000000..0c7e95530 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/BridgedResolver.java @@ -0,0 +1,94 @@ +package org.jivesoftware.smackx.jingle.nat; + +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; + +import java.util.Random; + +/** + * Bridged Resolver use a RTPBridge Service to add a relayed candidate. + * A very reliable solution for NAT Traversal. + * + * The resolver verify is the XMPP Server that the client is connected offer this service. + * If the server supports, a candidate is requested from the service. + * The resolver adds this candidate + */ +public class BridgedResolver extends TransportResolver{ + + XMPPConnection connection; + + Random random = new Random(); + + long sid; + + /** + * Constructor. + * A Bridged Resolver need a XMPPConnection to connect to a RTP Bridge. + */ + public BridgedResolver(XMPPConnection connection) { + super(); + this.connection = connection; + } + + /** + * Resolve Bridged Candidate. + *

+ * The BridgedResolver takes the IP addresse and ports of a jmf proxy service. + */ + public synchronized void resolve() throws XMPPException { + + setResolveInit(); + + clearCandidates(); + + sid = Math.abs(random.nextLong()); + + RTPBridge rtpBridge = RTPBridge.getRTPBridge(connection, String.valueOf(sid)); + + BasicResolver basicResolver = new BasicResolver(); + + basicResolver.initializeAndWait(); + basicResolver.resolve(); + + TransportCandidate localCandidate = new TransportCandidate.Fixed( + rtpBridge.getIp(), rtpBridge.getPortA()); + localCandidate.setLocalIp(basicResolver.getCandidate(0).getLocalIp()); + + TransportCandidate remoteCandidate = new TransportCandidate.Fixed( + rtpBridge.getIp(), rtpBridge.getPortB()); + remoteCandidate.setLocalIp(basicResolver.getCandidate(0).getLocalIp()); + + localCandidate.setSymmetric(remoteCandidate); + remoteCandidate.setSymmetric(localCandidate); + + localCandidate.setPassword(rtpBridge.getPass()); + remoteCandidate.setPassword(rtpBridge.getPass()); + + localCandidate.setSessionId(rtpBridge.getSid()); + remoteCandidate.setSessionId(rtpBridge.getSid()); + + localCandidate.setConnection(this.connection); + remoteCandidate.setConnection(this.connection); + + addCandidate(localCandidate); + + setResolveEnd(); + } + + public void initialize() throws XMPPException { + + clearCandidates(); + + if (!RTPBridge.serviceAvailable(connection)) { + setInitialized(); + throw new XMPPException("No RTP Bridge service available"); + } + setInitialized(); + + } + + public void cancel() throws XMPPException { + // Nothing to do here + } + +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/BridgedTransportManager.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/BridgedTransportManager.java new file mode 100644 index 000000000..e9d083397 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/BridgedTransportManager.java @@ -0,0 +1,75 @@ +package org.jivesoftware.smackx.jingle.nat; + +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.jingle.JingleSession; +import org.jivesoftware.smackx.jingle.listeners.CreatedJingleSessionListener; +import org.jivesoftware.smackx.jingle.listeners.JingleSessionListener; +import org.jivesoftware.smackx.jingle.media.PayloadType; + +/** + * $RCSfile$ + * $Revision: $ + * $Date: 15/11/2006 + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A Jingle Transport Manager implementation to be used for NAT Networks. + * This kind of transport needs that the connected XMPP Server provide a Bridge Service. (http://www.jivesoftware.com/protocol/rtpbridge) + * To relay the jmf outside the NAT. + * + * @author Thiago Camargo + */ +public class BridgedTransportManager extends JingleTransportManager implements JingleSessionListener, CreatedJingleSessionListener { + + XMPPConnection xmppConnection; + + public BridgedTransportManager(XMPPConnection xmppConnection) { + super(); + this.xmppConnection = xmppConnection; + } + + // Return the correspondent resolver + protected TransportResolver createResolver() { + BridgedResolver bridgedResolver = new BridgedResolver(this.xmppConnection); + return bridgedResolver; + } + + // Implement a Session Listener to relay candidates after establishment + + public void sessionEstablished(PayloadType pt, TransportCandidate rc, TransportCandidate lc, JingleSession jingleSession) { + RTPBridge rtpBridge = RTPBridge.relaySession(lc.getConnection(), lc.getSessionId(), lc.getPassword(), rc, lc); + } + + public void sessionDeclined(String reason, JingleSession jingleSession) { + } + + public void sessionRedirected(String redirection, JingleSession jingleSession) { + } + + public void sessionClosed(String reason, JingleSession jingleSession) { + } + + public void sessionClosedOnError(XMPPException e, JingleSession jingleSession) { + } + + // Session Created + + public void sessionCreated(JingleSession jingleSession) { + jingleSession.addListener(this); + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/FixedResolver.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/FixedResolver.java new file mode 100644 index 000000000..0bae13aaa --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/FixedResolver.java @@ -0,0 +1,67 @@ +package org.jivesoftware.smackx.jingle.nat; + +import org.jivesoftware.smack.XMPPException; + +/** + * The FixedResolver is a resolver where + * the external address and port are previously known when the object is + * initialized. + * + * @author Alvaro Saurin + */ +public class FixedResolver extends TransportResolver { + + TransportCandidate fixedCandidate; + + /** + * Constructor. + */ + public FixedResolver(String ip, int port) { + super(); + setFixedCandidate(ip, port); + } + + /** + * Create a basic resolver, where we provide the IP and port. + * + * @param ip an IP address + * @param port a port + */ + public void setFixedCandidate(String ip, int port) { + System.out.println("FIXED"); + fixedCandidate = new TransportCandidate.Fixed(ip, port); + } + + /** + * Resolve the IP address. + */ + public synchronized void resolve() throws XMPPException { + if (!isResolving()) { + setResolveInit(); + + clearCandidates(); + + if (fixedCandidate.getLocalIp() == null) + fixedCandidate.setLocalIp(fixedCandidate.getIp()); + + if (fixedCandidate != null) { + addCandidate(fixedCandidate); + } + + setResolveEnd(); + } + } + + /** + * Initialize the resolver. + * + * @throws XMPPException + */ + public void initialize() throws XMPPException { + setInitialized(); + } + + public void cancel() throws XMPPException { + // Nothing to do here + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/ICEResolver.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/ICEResolver.java new file mode 100644 index 000000000..9de788309 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/ICEResolver.java @@ -0,0 +1,85 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2005 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.jingle.nat; + +import de.javawi.jstun.test.demo.ice.Candidate; +import de.javawi.jstun.test.demo.ice.ICENegociator; +import de.javawi.jstun.util.UtilityException; +import org.jivesoftware.smack.XMPPException; + +import java.net.UnknownHostException; +import java.util.List; + +/** + * ICE Resolver for Jingle transport method that results in sending data between two entities using the Interactive Connectivity Establishment (ICE) methodology. (XEP-0176) + * The goal of this resolver is to make possible to establish and manage out-of-band connections between two XMPP entities, even if they are behind Network Address Translators (NATs) or firewalls. + * To use this resolver you must have a STUN Server and be in a non STUN blocked network. + * + * @author Thiago Camargo + */ +public class ICEResolver extends TransportResolver { + + public ICEResolver() { + super(); + this.setType(Type.ice); + } + + public void initialize() throws XMPPException { + if (!isResolving() && !isResolved()) { + System.out.println("Initialized"); + + ICENegociator cc = new ICENegociator((short) 1); + // gather candidates + cc.gatherCandidateAddresses(); + // priorize candidates + cc.prioritizeCandidates(); + // get SortedCandidates + //List sortedCandidates = cc.getSortedCandidates(); + + for (Candidate candidate : cc.getSortedCandidates()) + try { + TransportCandidate transportCandidate = new TransportCandidate.Ice(candidate.getAddress().getInetAddress().getHostAddress(), 1, candidate.getNetwork(), "1", candidate.getPort(), "1", candidate.getPriority()); + transportCandidate.setLocalIp(candidate.getBase().getAddress().getInetAddress().getHostAddress()); + this.addCandidate(transportCandidate); + System.out.println("C: " + candidate.getAddress().getInetAddress() + "|" + candidate.getBase().getAddress().getInetAddress() + " p:" + candidate.getPriority()); + } catch (UtilityException e) { + e.printStackTrace(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + } + this.setInitialized(); + } + + public void cancel() throws XMPPException { + + } + + /** + * Resolve the IP and obtain a valid transport method. + */ + public synchronized void resolve() throws XMPPException { + this.setResolveInit(); + System.out.println("Resolve"); + this.setResolveEnd(); + } + +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/ICETransportManager.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/ICETransportManager.java new file mode 100644 index 000000000..b21e54f54 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/ICETransportManager.java @@ -0,0 +1,46 @@ +package org.jivesoftware.smackx.jingle.nat; + +import org.jivesoftware.smack.XMPPException; + +/** + * $RCSfile$ + * $Revision: $ + * $Date: 02/01/2007 + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +public class ICETransportManager extends JingleTransportManager { + + ICEResolver iceResolver = null; + + public ICETransportManager() { + iceResolver = new ICEResolver(); + try { + iceResolver.initializeAndWait(); + } catch (XMPPException e) { + e.printStackTrace(); + } + } + + protected TransportResolver createResolver() { + try { + iceResolver.resolve(); + } catch (XMPPException e) { + e.printStackTrace(); + } + return iceResolver; + } + +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/JingleTransportManager.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/JingleTransportManager.java new file mode 100644 index 000000000..3eb3d9f68 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/JingleTransportManager.java @@ -0,0 +1,54 @@ +package org.jivesoftware.smackx.jingle.nat; + +import org.jivesoftware.smack.XMPPException; + +/** + * Transport manager for Jingle. + * + * This class makes easier the use of transport resolvers by presenting a simple + * interface for algorithm selection. The transport manager also keeps the match + * between the resolution method and the <transport> element present in + * Jingle packets. + * + * As Jingle have many transport methods (official and unofficial methods), + * this abstract class helps us to extends the transport support of the API. + * + * This class must be used with a JingleManager instance in the following way: + * + * JingleManager jingleManager = new JingleManager(xmppConnection, new BasicTransportManager()); + * + * @author Thiago Camargo + */ +public abstract class JingleTransportManager { + // This class implements the context of a Strategy pattern... + + /** + * Deafult contructor. + */ + public JingleTransportManager() { + + } + + /** + * Get a new Transport Resolver to be used in a Jingle Session + * + * @return + */ + public TransportResolver getResolver() throws XMPPException { + TransportResolver resolver = createResolver(); + if (resolver == null) { + resolver = new BasicResolver(); + } + resolver.initializeAndWait(); + + return resolver; + } + + /** + * Create a Transport Resolver instance according to the implementation. + * + * @return + */ + protected abstract TransportResolver createResolver(); + +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/RTPBridge.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/RTPBridge.java new file mode 100644 index 000000000..fa85f3d6c --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/RTPBridge.java @@ -0,0 +1,462 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2005 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.jingle.nat; + +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smack.provider.ProviderManager; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.packet.DiscoverItems; +import org.xmlpull.v1.XmlPullParser; + +import java.util.Iterator; + +/** + * RTPBridge IQ Packet used to request and retrieve a RTPBridge Candidates that can be used for a Jingle Media Transmission between two parties that are behind NAT. + * This Jingle Bridge has all the needed information to establish a full UDP Channel (Send and Receive) between two parties. + * This transport method should be used only if other transport methods are not allowed. Or if you want a more reliable transport. + *

+ * High Level Usage Example: + *

+ * RTPBridge rtpBridge = RTPBridge.getRTPBridge(xmppConnection, sessionID); + * + * @author Thiago Camargo + */ +public class RTPBridge extends IQ { + + private String sid; + private String pass; + private String ip; + private String name; + private int portA = -1; + private int portB = -1; + private String hostA; + private String hostB; + private BridgeAction bridgeAction = BridgeAction.create; + + private enum BridgeAction { + create, change + } + + /** + * Element name of the packet extension. + */ + public static final String NAME = "rtpbridge"; + + /** + * Element name of the packet extension. + */ + public static final String ELEMENT_NAME = "rtpbridge"; + + /** + * Namespace of the packet extension. + */ + public static final String NAMESPACE = "http://www.jivesoftware.com/protocol/rtpbridge"; + + static { + ProviderManager.getInstance().addIQProvider(NAME, NAMESPACE, new Provider()); + } + + /** + * Creates a RTPBridge Instance with defined Session ID + * + * @param sid + */ + public RTPBridge(String sid) { + this.sid = sid; + } + + /** + * Creates a RTPBridge Instance with defined Session ID + * + * @param sid + * @param bridgeAction + */ + public RTPBridge(String sid, BridgeAction bridgeAction) { + this.sid = sid; + this.bridgeAction = bridgeAction; + } + + /** + * Creates a RTPBridge Packet without Session ID + */ + public RTPBridge() { + } + + /** + * Get the attributes string + */ + public String getAttributes() { + StringBuilder str = new StringBuilder(); + + if (getSid() != null) + str.append(" sid='").append(getSid()).append("'"); + + if (getPass() != null) + str.append(" pass='").append(getPass()).append("'"); + + if (getPortA() != -1) + str.append(" porta='").append(getPortA()).append("'"); + + if (getPortB() != -1) + str.append(" portb='").append(getPortB()).append("'"); + + if (getHostA() != null) + str.append(" hosta='").append(getHostA()).append("'"); + + if (getHostB() != null) + str.append(" hostb='").append(getHostB()).append("'"); + + return str.toString(); + } + + /** + * Get the Session ID of the Packet (usually same as Jingle Session ID) + * + * @return + */ + public String getSid() { + return sid; + } + + /** + * Set the Session ID of the Packet (usually same as Jingle Session ID) + * + * @param sid + */ + public void setSid(String sid) { + this.sid = sid; + } + + /** + * Get the Host A IP Address + * + * @return + */ + public String getHostA() { + return hostA; + } + + /** + * Set the Host A IP Address + * + * @param hostA + */ + public void setHostA(String hostA) { + this.hostA = hostA; + } + + /** + * Get the Host B IP Address + * + * @return + */ + public String getHostB() { + return hostB; + } + + /** + * Set the Host B IP Address + * + * @param hostB + */ + public void setHostB(String hostB) { + this.hostB = hostB; + } + + /** + * Get Side A receive port + * + * @return + */ + public int getPortA() { + return portA; + } + + /** + * Set Side A receive port + * + * @param portA + */ + public void setPortA(int portA) { + this.portA = portA; + } + + /** + * Get Side B receive port + * + * @return + */ + public int getPortB() { + return portB; + } + + /** + * Set Side B receive port + * + * @param portB + */ + public void setPortB(int portB) { + this.portB = portB; + } + + /** + * Get the RTP Bridge IP + * + * @return + */ + public String getIp() { + return ip; + } + + /** + * Set the RTP Bridge IP + * + * @param ip + */ + public void setIp(String ip) { + this.ip = ip; + } + + /** + * Get the RTP Agent Pass + * + * @return + */ + public String getPass() { + return pass; + } + + /** + * Set the RTP Agent Pass + * + * @param pass + */ + public void setPass(String pass) { + this.pass = pass; + } + + /** + * Get the name of the Candidate + * + * @return + */ + public String getName() { + return name; + } + + /** + * Set the name of the Candidate + * + * @param name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Get the Child Element XML of the Packet + * + * @return + */ + public String getChildElementXML() { + StringBuilder str = new StringBuilder(); + str.append("<" + ELEMENT_NAME + " xmlns='" + NAMESPACE + "' sid='").append(sid).append("'>"); + + if (bridgeAction.equals(BridgeAction.create)) + str.append(""); + else + str.append(""); + + str.append(""); + return str.toString(); + } + + /** + * IQProvider for RTP Bridge packets. + * Parse receive RTPBridge packet to a RTPBridge instance + * + * @author Thiago Rocha + */ + public static class Provider implements IQProvider { + + public Provider() { + super(); + } + + public IQ parseIQ(XmlPullParser parser) throws Exception { + + boolean done = false; + + int eventType; + String elementName; + String namespace; + + if (!parser.getNamespace().equals(RTPBridge.NAMESPACE)) + throw new Exception("Not a RTP Bridge packet"); + + RTPBridge iq = new RTPBridge(); + + for (int i = 0; i < parser.getAttributeCount(); i++) { + if (parser.getAttributeName(i).equals("sid")) + iq.setSid(parser.getAttributeValue(i)); + } + + // Start processing sub-elements + while (!done) { + eventType = parser.next(); + elementName = parser.getName(); + namespace = parser.getNamespace(); + + if (eventType == XmlPullParser.START_TAG) { + if (elementName.equals("candidate")) { + for (int i = 0; i < parser.getAttributeCount(); i++) { + if (parser.getAttributeName(i).equals("ip")) + iq.setIp(parser.getAttributeValue(i)); + else if (parser.getAttributeName(i).equals("pass")) + iq.setPass(parser.getAttributeValue(i)); + else if (parser.getAttributeName(i).equals("name")) + iq.setName(parser.getAttributeValue(i)); + else if (parser.getAttributeName(i).equals("porta")) + iq.setPortA(Integer.parseInt(parser.getAttributeValue(i))); + else if (parser.getAttributeName(i).equals("portb")) + iq.setPortB(Integer.parseInt(parser.getAttributeValue(i))); + } + } + + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(RTPBridge.ELEMENT_NAME)) { + done = true; + } + } + } + return iq; + } + } + + /** + * Get a new RTPBridge Candidate from the server. + * If a error occurs or the server donīt support RTPBridge Service, null is returned. + * + * @param xmppConnection + * @param sessionID + * @return + */ + public static RTPBridge getRTPBridge(XMPPConnection xmppConnection, String sessionID) { + + if (!xmppConnection.isConnected()) { + return null; + } + + RTPBridge rtpPacket = new RTPBridge(sessionID); + rtpPacket.setTo(RTPBridge.NAME + "." + xmppConnection.getServiceName()); + + PacketCollector collector = xmppConnection + .createPacketCollector(new PacketIDFilter(rtpPacket.getPacketID())); + + xmppConnection.sendPacket(rtpPacket); + + RTPBridge response = (RTPBridge) collector + .nextResult(SmackConfiguration.getPacketReplyTimeout()); + + // Cancel the collector. + collector.cancel(); + + return response; + } + + /** + * Check if the server support RTPBridge Service. + * + * @param xmppConnection + * @return + */ + public static boolean serviceAvailable(XMPPConnection xmppConnection) { + + if (!xmppConnection.isConnected()) { + return false; + } + + System.out.println("service listing"); + + ServiceDiscoveryManager disco = ServiceDiscoveryManager + .getInstanceFor(xmppConnection); + try { + DiscoverItems items = disco.discoverItems(xmppConnection.getServiceName()); + Iterator iter = items.getItems(); + while (iter.hasNext()) { + DiscoverItems.Item item = (DiscoverItems.Item) iter.next(); + if (item.getEntityID().startsWith("rtpbridge.")) { + return true; + } + } + } + catch (XMPPException e) { + e.printStackTrace(); + } + + return false; + } + + /** + * Check if the server support RTPBridge Service. + * + * @param xmppConnection + * @return + */ + public static RTPBridge relaySession(XMPPConnection xmppConnection, String sessionID, String pass, TransportCandidate proxyCandidate, TransportCandidate localCandidate) { + + if (!xmppConnection.isConnected()) { + return null; + } + + RTPBridge rtpPacket = new RTPBridge(sessionID, RTPBridge.BridgeAction.change); + rtpPacket.setTo(RTPBridge.NAME + "." + xmppConnection.getServiceName()); + rtpPacket.setType(Type.SET); + + rtpPacket.setPass(pass); + rtpPacket.setPortA(localCandidate.getPort()); + rtpPacket.setPortB(proxyCandidate.getPort()); + rtpPacket.setHostA(localCandidate.getIp()); + rtpPacket.setHostB(proxyCandidate.getIp()); + + // System.out.println("Relayed to: " + candidate.getIp() + ":" + candidate.getPort()); + + PacketCollector collector = xmppConnection + .createPacketCollector(new PacketIDFilter(rtpPacket.getPacketID())); + + xmppConnection.sendPacket(rtpPacket); + + RTPBridge response = (RTPBridge) collector + .nextResult(SmackConfiguration.getPacketReplyTimeout()); + + // Cancel the collector. + collector.cancel(); + + return response; + } + +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/STUNResolver.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/STUNResolver.java new file mode 100644 index 000000000..8e77dfbc1 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/STUNResolver.java @@ -0,0 +1,509 @@ +package org.jivesoftware.smackx.jingle.nat; + +import de.javawi.jstun.test.BindingLifetimeTest; +import de.javawi.jstun.test.DiscoveryInfo; +import de.javawi.jstun.test.DiscoveryTest; +import org.jivesoftware.smack.XMPPException; +import org.xmlpull.mxp1.MXParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; + +/** + * Transport resolver using the JSTUN library, to discover public IP and use it as a candidate. + * + * The goal of this resolver is to take possible to establish and manage out-of-band connections between two XMPP entities, even if they are behind Network Address Translators (NATs) or firewalls. + * + * @author Alvaro Saurin + */ +public class STUNResolver extends TransportResolver { + + // The filename where the STUN servers are stored. + public final static String STUNSERVERS_FILENAME = "META-INF/stun-config.xml"; + + // Fallback values when we don't have any STUN server to use... + private final static String FALLBACKHOSTNAME = "stun.xten.net"; + + private final static int FALLBACKHOSTPORT = 3478; + + // Current STUN server we are using + protected STUNService currentServer; + + protected Thread resolverThread; + + protected int defaultPort; + + protected String resolvedPublicIP; + protected String resolvedLocalIP; + + /** + * Constructor with default STUN server. + */ + public STUNResolver() { + super(); + + this.defaultPort = 0; + this.currentServer = new STUNService(); + } + + /** + * Constructor with a default port. + * + * @param defaultPort Port to use by default. + */ + public STUNResolver(int defaultPort) { + this(); + + this.defaultPort = defaultPort; + } + + /** + * Return true if the service is working. + * + * @see TransportResolver#isResolving() + */ + public boolean isResolving() { + return super.isResolving() && resolverThread != null; + } + + /** + * Set the STUN server name and port + * + * @param ip the STUN server name + * @param port the STUN server port + */ + public void setSTUNService(String ip, int port) { + currentServer = new STUNService(ip, port); + } + + /** + * Get the name of the current STUN server. + * + * @return the name of the STUN server + */ + public String getCurrentServerName() { + if (!currentServer.isNull()) { + return currentServer.getHostname(); + } else { + return null; + } + } + + /** + * Get the port of the current STUN server. + * + * @return the port of the STUN server + */ + public int getCurrentServerPort() { + if (!currentServer.isNull()) { + return currentServer.getPort(); + } else { + return 0; + } + } + + /** + * Load the STUN configuration from a stream. + * + * @param stunConfigStream An InputStream with the configuration file. + * @return A list of loaded servers + */ + public ArrayList loadSTUNServers(java.io.InputStream stunConfigStream) { + ArrayList serversList = new ArrayList(); + String serverName; + int serverPort; + + try { + XmlPullParser parser = new MXParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(stunConfigStream, "UTF-8"); + + int eventType = parser.getEventType(); + do { + if (eventType == XmlPullParser.START_TAG) { + + // Parse a STUN server definition + if (parser.getName().equals("stunServer")) { + + serverName = null; + serverPort = -1; + + // Parse the hostname + parser.next(); + parser.next(); + serverName = parser.nextText(); + + // Parse the port + parser.next(); + parser.next(); + try { + serverPort = Integer.parseInt(parser.nextText()); + } + catch (Exception e) { + } + + // If we have a valid hostname and port, add + // it to the list. + if (serverName != null && serverPort != -1) { + STUNService service = new STUNService(serverName, serverPort); + + serversList.add(service); + } + } + } + eventType = parser.next(); + + } + while (eventType != XmlPullParser.END_DOCUMENT); + + } + catch (XmlPullParserException e) { + e.printStackTrace(); + } + catch (IOException e) { + e.printStackTrace(); + } + + currentServer = bestSTUNServer(serversList); + + return serversList; + } + + /** + * Load a list of services: STUN servers and ports. Some public STUN servers + * are: + *

+ *

+     *               iphone-stun.freenet.de:3478
+     *               larry.gloo.net:3478
+     *               stun.xten.net:3478
+     *               stun.fwdnet.net
+     *               stun.fwd.org (no DNS SRV record)
+     *               stun01.sipphone.com (no DNS SRV record)
+     *               stun.softjoys.com (no DNS SRV record)
+     *               stun.voipbuster.com (no DNS SRV record)
+     *               stun.voxgratia.org (no DNS SRV record)
+     *               stun.noc.ams-ix.net
+     * 
+ *

+ * This list should be contained in a file in the "META-INF" directory + * + * @return a list of services + */ + public ArrayList loadSTUNServers() { + ArrayList serversList = new ArrayList(); + + // Load the STUN configuration + try { + // Get an array of class loaders to try loading the config from. + ClassLoader[] classLoaders = new ClassLoader[2]; + classLoaders[0] = new STUNResolver() { + }.getClass().getClassLoader(); + classLoaders[1] = Thread.currentThread().getContextClassLoader(); + + for (int i = 0; i < classLoaders.length; i++) { + Enumeration stunConfigEnum = classLoaders[i] + .getResources(STUNSERVERS_FILENAME); + + while (stunConfigEnum.hasMoreElements() && serversList.isEmpty()) { + URL url = (URL) stunConfigEnum.nextElement(); + java.io.InputStream stunConfigStream = null; + + stunConfigStream = url.openStream(); + serversList.addAll(loadSTUNServers(stunConfigStream)); + stunConfigStream.close(); + } + } + } + catch (Exception e) { + e.printStackTrace(); + } + + // If the list of candidates is empty, add at least one default server + if (serversList.isEmpty()) { + currentServer = new STUNService(FALLBACKHOSTNAME, FALLBACKHOSTPORT); + serversList.add(currentServer); + } + + return serversList; + } + + /** + * Get the best usable STUN server from a list. + * + * @return the best STUN server that can be used. + */ + private STUNService bestSTUNServer(ArrayList listServers) { + if (listServers.isEmpty()) { + return null; + } else { + // TODO: this should use some more advanced criteria... + return (STUNService) listServers.get(0); + } + } + + /** + * Resolve the IP and obtain a valid transport method. + */ + public synchronized void resolve() throws XMPPException { + + setResolveInit(); + + clearCandidates(); + + TransportCandidate candidate = new TransportCandidate.Fixed( + resolvedPublicIP, getFreePort()); + candidate.setLocalIp(resolvedLocalIP); + + System.out.println("RESOLVING : " + resolvedPublicIP + ":" + candidate.getPort()); + + addCandidate(candidate); + + setResolveEnd(); + + } + + /** + * Initialize the resolver. + * + * @throws XMPPException + */ + public void initialize() throws XMPPException { + System.out.println("Initialized"); + if (!isResolving()&&!isResolved()) { + // Get the best STUN server available + if (currentServer.isNull()) { + loadSTUNServers(); + } + // We should have a valid STUN server by now... + if (!currentServer.isNull()) { + + clearCandidates(); + + resolverThread = new Thread(new Runnable() { + public void run() { + // Iterate through the list of interfaces, and ask + // to the STUN server for our address. + try { + Enumeration ifaces = NetworkInterface.getNetworkInterfaces(); + String candAddress; + int candPort; + + while (ifaces.hasMoreElements()) { + + NetworkInterface iface = (NetworkInterface) ifaces + .nextElement(); + Enumeration iaddresses = iface.getInetAddresses(); + + while (iaddresses.hasMoreElements()) { + InetAddress iaddress = (InetAddress) iaddresses + .nextElement(); + if (!iaddress.isLoopbackAddress() + && !iaddress.isLinkLocalAddress()) { + + // Reset the candidate + candAddress = null; + candPort = -1; + + DiscoveryTest test = new DiscoveryTest(iaddress, + currentServer.getHostname(), + currentServer.getPort()); + try { + // Run the tests and get the + // discovery + // information, where all the + // info is stored... + DiscoveryInfo di = test.test(); + + candAddress = di.getPublicIP() != null ? + di.getPublicIP().getHostAddress() : null; + + // Get a valid port + if (defaultPort == 0) { + candPort = getFreePort(); + } else { + candPort = defaultPort; + } + + // If we have a valid candidate, + // add it to the list. + if (candAddress != null && candPort >= 0) { + TransportCandidate candidate = new TransportCandidate.Fixed( + candAddress, candPort); + candidate.setLocalIp(iaddress.getHostAddress() != null ? iaddress.getHostAddress() : iaddress.getHostName()); + addCandidate(candidate); + + resolvedPublicIP = candidate.getIp(); + resolvedLocalIP = candidate.getLocalIp(); + return; + } + } + catch (Exception e) { + e.printStackTrace(); + } + } + } + } + } + catch (SocketException e) { + e.printStackTrace(); + } + finally { + setInitialized(); + } + } + }, "Waiting for all the transport candidates checks..."); + + resolverThread.setName("STUN resolver"); + resolverThread.start(); + } else { + throw new IllegalStateException("No valid STUN server found."); + } + } + } + + /** + * Cancel any operation. + * + * @see TransportResolver#cancel() + */ + public synchronized void cancel() throws XMPPException { + if (isResolving()) { + resolverThread.interrupt(); + setResolveEnd(); + } + } + + /** + * Clear the list of candidates and start the resolution again. + * + * @see TransportResolver#clear() + */ + public synchronized void clear() throws XMPPException { + this.defaultPort = 0; + super.clear(); + } + + /** + * STUN service definition. + */ + protected class STUNService { + + private String hostname; // The hostname of the service + + private int port; // The port number + + /** + * Basic constructor, with the hostname and port + * + * @param hostname The hostname + * @param port The port + */ + public STUNService(String hostname, int port) { + super(); + + this.hostname = hostname; + this.port = port; + } + + /** + * Default constructor, without name and port. + */ + public STUNService() { + this(null, -1); + } + + /** + * Get the host name of the STUN service. + * + * @return The host name + */ + public String getHostname() { + return hostname; + } + + /** + * Set the hostname of the STUN service. + * + * @param hostname The host name of the service. + */ + public void setHostname(String hostname) { + this.hostname = hostname; + } + + /** + * Get the port of the STUN service + * + * @return The port number where the STUN server is waiting. + */ + public int getPort() { + return port; + } + + /** + * Set the port number for the STUN service. + * + * @param port The port number. + */ + public void setPort(int port) { + this.port = port; + } + + /** + * Basic format test: the service is not null. + * + * @return true if the hostname and port are null + */ + public boolean isNull() { + if (hostname == null) { + return true; + } else if (hostname.length() == 0) { + return true; + } else if (port < 0) { + return true; + } else { + return false; + } + } + + /** + * Check a binding with the STUN currentServer. + *

+ * Note: this function blocks for some time, waiting for a response. + * + * @return true if the currentServer is usable. + */ + public boolean checkBinding() { + boolean result = false; + + try { + BindingLifetimeTest binding = new BindingLifetimeTest(hostname, port); + + binding.test(); + + while (true) { + Thread.sleep(5000); + if (binding.getLifetime() != -1) { + if (binding.isCompleted()) { + return true; + } + } else { + break; + } + } + } + catch (Exception e) { + e.printStackTrace(); + } + + return result; + } + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/STUNTransportManager.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/STUNTransportManager.java new file mode 100644 index 000000000..e519beac4 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/STUNTransportManager.java @@ -0,0 +1,51 @@ +package org.jivesoftware.smackx.jingle.nat; + +import org.jivesoftware.smack.XMPPException; + +/** + * $RCSfile$ + * $Revision: $ + * $Date: 15/11/2006 + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A Jingle Transport Manager implementation to be used on NAT networks with STUN Service NOT Blocked. + * + * @author Thiago Camargo + */ +public class STUNTransportManager extends JingleTransportManager { + STUNResolver stunResolver = null; + + public STUNTransportManager() { + stunResolver = new STUNResolver() { + }; + try { + stunResolver.initializeAndWait(); + } catch (XMPPException e) { + e.printStackTrace(); + } + } + + protected TransportResolver createResolver() { + try { + stunResolver.resolve(); + } catch (XMPPException e) { + e.printStackTrace(); + } + return stunResolver; + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TransportCandidate.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TransportCandidate.java new file mode 100644 index 000000000..849fb25c6 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TransportCandidate.java @@ -0,0 +1,855 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright (C) 2002-2006 Jive Software. All rights reserved. + * ==================================================================== + * The Jive Software License (based on Apache Software License, Version 1.1) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by + * Jive Software (http://www.jivesoftware.com)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Smack" and "Jive Software" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please + * contact webmaster@jivesoftware.com. + * + * 5. Products derived from this software may not be called "Smack", + * nor may "Smack" appear in their name, without prior written + * permission of Jive Software. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL JIVE SOFTWARE OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + */ + +package org.jivesoftware.smackx.jingle.nat; + +import org.jivesoftware.smack.XMPPConnection; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; + +/** + * Transport candidate. + * + * A candidate represents the possible transport for data interchange between + * the two endpoints. + * + * @author Thiago Camargo + * @author Alvaro Saurin + */ +public abstract class TransportCandidate { + + private String name; + + private String ip; // IP address + + private int port; // Port to use, or 0 for any port + + private String localIp; + + private int generation; + + protected String password; + + private String sessionId; + + private XMPPConnection connection; + + private TransportCandidate symmetric; + + // Listeners for events + private final List listeners = new ArrayList(); + + public String getIp() { + return ip; + } + + /** + * Set the IP address. + * + * @param ip the IP address + */ + public void setIp(String ip) { + this.ip = ip; + } + + /** + * Get local IP to bind to this candidate + * + * @return + */ + public String getLocalIp() { + return localIp == null ? ip : localIp; + } + + /** + * Set local IP to bind to this candidate + * + * @param localIp + */ + public void setLocalIp(String localIp) { + this.localIp = localIp; + } + + /** + * Get the symetric candidate for this candidate if it exists. + * + * @return + */ + public TransportCandidate getSymmetric() { + return symmetric; + } + + /** + * Set the symetric candidate for this candidate. + * + * @param symetric + */ + public void setSymmetric(TransportCandidate symetric) { + this.symmetric = symetric; + } + + /** + * Get the password used by ICE or relayed candidate + * + * @return a password + */ + public String getPassword() { + return password; + } + + /** + * Set the password used by ICE or relayed candidate + * + * @param password a password + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * Get the XMPPConnection use to send or receive this candidate + * + * @return + */ + public XMPPConnection getConnection() { + return connection; + } + + /** + * Set the XMPPConnection use to send or receive this candidate + * + * @param connection + */ + public void setConnection(XMPPConnection connection) { + this.connection = connection; + } + + /** + * Get the jingleīs sessionId that is using this candidate + * + * @return + */ + public String getSessionId() { + return sessionId; + } + + /** + * Set the jingleīs sessionId that is using this candidate + * + * @param sessionId + */ + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + /** + * Empty constructor + */ + public TransportCandidate() { + this(null, 0, 0); + } + + /** + * Constructor with IP address and port + * + * @param ip The IP address. + * @param port The port number. + */ + public TransportCandidate(String ip, int port) { + this(ip, port, 0); + } + + /** + * Constructor with IP address and port + * + * @param ip The IP address. + * @param port The port number. + * @param generation The generation + */ + public TransportCandidate(String ip, int port, int generation) { + this.ip = ip; + this.port = port; + this.generation = generation; + } + + /** + * Return true if the candidate is not valid. + * + * @return true if the candidate is null. + */ + public boolean isNull() { + if (ip == null) { + return true; + } else if (ip.length() == 0) { + return true; + } else if (port < 0) { + return true; + } else { + return false; + } + } + + /** + * Get the port, or 0 for any port. + * + * @return the port or 0 + */ + public int getPort() { + return port; + } + + /** + * Set the port, using 0 for any port + * + * @param port the port + */ + public void setPort(int port) { + this.port = port; + } + + /** + * Get the generation for a transportElement definition + * + * @return the generation + */ + public int getGeneration() { + return generation; + } + + /** + * Set the generation for a transportElement definition. + * + * @param generation the generation number + */ + public void setGeneration(int generation) { + this.generation = generation; + } + + /** + * Get the name used for identifying this transportElement method (optional) + * + * @return a name used for identifying this transportElement (ie, + * "myrtpvoice1") + */ + public String getName() { + return name; + } + + /** + * Set a name for identifying this transportElement. + * + * @param name the name used for the transportElement + */ + public void setName(String name) { + this.name = name; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final TransportCandidate other = (TransportCandidate) obj; + if (generation != other.generation) { + return false; + } + if (getIp() == null) { + if (other.getIp() != null) { + return false; + } + } else if (!getIp().equals(other.getIp())) { + return false; + } + if (getName() == null) { + if (other.getName() != null) { + return false; + } + } else if (!getName().equals(other.getName())) { + return false; + } + if (getPort() != other.getPort()) { + return false; + } + return true; + } + + + /** + * Check if a transport candidate is usable. The transport resolver should + * check if the transport candidate the other endpoint has provided is + * usable. + *

+ * This method provides a basic check where it sends a "ping" to the remote + * address provided in the candidate. If the "ping" succedess, the candidate + * is accepted. Subclasses should provide better methods if they can... + */ + public void check() { + //TODO candidate is being checked trigger + //candidatesChecking.add(cand); + + Thread checkThread = new Thread(new Runnable() { + public void run() { + boolean isUsable; + + InetAddress candAddress; + try { + candAddress = InetAddress.getByName(getIp()); + isUsable = true;//candAddress.isReachable(CHECK_TIMEOUT); + } catch (Exception e) { + isUsable = false; + } + triggerCandidateChecked(isUsable); + + //TODO candidate is being checked trigger + //candidatesChecking.remove(cand); + } + }, "Transport candidate check"); + + checkThread.setName("Transport candidate test"); + checkThread.start(); + } + + /** + * Trigger a new candidate checked event. + * + * @param result The result. + */ + private void triggerCandidateChecked(boolean result) { + + for (TransportResolverListener.Checker trl : getListenersList()) { + trl.candidateChecked(this, result); + } + } + + /** + * Get the list of listeners + * + * @return the list of listeners + */ + public List getListenersList() { + synchronized (listeners) { + return new ArrayList(listeners); + } + } + + /** + * Add a transport resolver listener. + * + * @param li The transport resolver listener to be added. + */ + public void addListener(TransportResolverListener.Checker li) { + synchronized (listeners) { + listeners.add(li); + } + } + + /** + * Fixed transport candidate + */ + public static class Fixed extends TransportCandidate { + + public Fixed() { + super(); + } + + /** + * Constructor with IP address and port + * + * @param ip The IP address. + * @param port The port number. + */ + public Fixed(String ip, int port) { + super(ip, port); + } + + /** + * Constructor with IP address and port + * + * @param ip The IP address. + * @param port The port number. + * @param generation The generation + */ + public Fixed(String ip, int port, int generation) { + super(ip, port, generation); + } + } + + /** + * Ice candidate. + */ + public static class Ice extends TransportCandidate implements Comparable { + + private String id; // An identification + + private String username; + + private int preference; + + private Protocol proto; + + private Channel channel; + + private int network; + + public Ice() { + super(); + } + + /** + * Constructor with the basic elements of a transport definition. + * + * @param ip the IP address to use as a local address + * @param generation used to keep track of the candidates + * @param network used for diagnostics (used when the machine has + * several NICs) + * @param password user name, as it is used in ICE + * @param port the port at the candidate IP address + * @param username user name, as it is used in ICE + * @param preference preference for this transportElement, as it is used + * in ICE + */ + public Ice(String ip, int generation, int network, + String password, int port, String username, + int preference) { + super(ip, port, generation); + + proto = Protocol.UDP; + channel = Channel.MYRTPVOICE; + + this.network = network; + this.password = password; + this.username = username; + this.preference = preference; + } + + /** + * Get the ID + * + * @return the id + */ + public String getId() { + return id; + } + + /** + * Set the ID + * + * @param id the id to set + */ + public void setId(String id) { + this.id = id; + } + + /** + * Get the protocol used for the transmission + * + * @return the protocol used for transmission + */ + public Protocol getProto() { + return proto; + } + + /** + * Set the protocol for the transmission + * + * @param proto the protocol to use + */ + public void setProto(Protocol proto) { + this.proto = proto; + } + + /** + * Get the network interface used for this connection + * + * @return the interface number + */ + public int getNetwork() { + return network; + } + + /** + * Set the interface for this connection + * + * @param network the interface number + */ + public void setNetwork(int network) { + this.network = network; + } + + /** + * Get the username for this transportElement in ICE + * + * @return a username string + */ + public String getUsername() { + return username; + } + + /** + * Get the channel + * + * @return the channel associated + */ + public Channel getChannel() { + return channel; + } + + /** + * Set the channel for this transportElement + * + * @param channel the new channel + */ + public void setChannel(Channel channel) { + this.channel = channel; + } + + /** + * Set the username for this transportElement in ICE + * + * @param username the username used in ICE + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * Get the preference number for this transportElement + * + * @return the preference for this transportElement + */ + public int getPreference() { + return preference; + } + + /** + * Set the preference order for this transportElement + * + * @param preference a number identifying the preference (as defined in + * ICE) + */ + public void setPreference(int preference) { + this.preference = preference; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final Ice other = (Ice) obj; + if (getChannel() == null) { + if (other.getChannel() != null) { + return false; + } + } else if (!getChannel().equals(other.getChannel())) { + return false; + } + if (getId() == null) { + if (other.getId() != null) { + return false; + } + } else if (!getId().equals(other.getId())) { + return false; + } + if (getNetwork() != other.getNetwork()) { + return false; + } + if (getPassword() == null) { + if (other.getPassword() != null) { + return false; + } + } else if (!getPassword().equals(other.password)) { + return false; + } + if (getPreference() != other.getPreference()) { + return false; + } + if (getProto() == null) { + if (other.getProto() != null) { + return false; + } + } else if (!getProto().equals(other.getProto())) { + return false; + } + if (getUsername() == null) { + if (other.getUsername() != null) { + return false; + } + } else if (!getUsername().equals(other.getUsername())) { + return false; + } + return true; + } + + public boolean isNull() { + if (super.isNull()) { + return true; + } else if (getProto().isNull()) { + return true; + } else if (getChannel().isNull()) { + return true; + } + return false; + } + + /** + * Compare the to other Transport candidate. + * + * @param arg another Transport candidate + * @return a negative integer, zero, or a positive integer as this + * object is less than, equal to, or greater than the specified + * object + */ + public int compareTo(Object arg) { + if (arg instanceof TransportCandidate.Ice) { + TransportCandidate.Ice tc = (TransportCandidate.Ice) arg; + if (getPreference() < tc.getPreference()) { + return -1; + } else if (getPreference() > tc.getPreference()) { + return 1; + } + } + return 0; + } + } + + /** + * Type-safe enum for the transportElement protocol + */ + public static class Protocol { + + public static final Protocol UDP = new Protocol("udp"); + + public static final Protocol TCP = new Protocol("tcp"); + + public static final Protocol TCPACT = new Protocol("tcp-act"); + + public static final Protocol TCPPASS = new Protocol("tcp-pass"); + + public static final Protocol SSLTCP = new Protocol("ssltcp"); + + private String value; + + public Protocol(String value) { + this.value = value; + } + + public String toString() { + return value; + } + + /** + * Returns the Protocol constant associated with the String value. + */ + public static Protocol fromString(String value) { + if (value == null) { + return UDP; + } + value = value.toLowerCase(); + if (value.equals("udp")) { + return UDP; + } else if (value.equals("tcp")) { + return TCP; + } else if (value.equals("tcp-act")) { + return TCPACT; + } else if (value.equals("tcp-pass")) { + return TCPPASS; + } else if (value.equals("ssltcp")) { + return SSLTCP; + } else { + return UDP; + } + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Protocol other = (Protocol) obj; + if (value == null) { + if (other.value != null) { + return false; + } + } else if (!value.equals(other.value)) { + return false; + } + return true; + } + + /** + * Return true if the protocol is not valid. + * + * @return true if the protocol is null + */ + public boolean isNull() { + if (value == null) { + return true; + } else if (value.length() == 0) { + return true; + } else { + return false; + } + } + } + + /** + * Type-safe enum for the transportElement channel + */ + public static class Channel { + + public static final Channel MYRTPVOICE = new Channel("myrtpvoice"); + + public static final Channel MYRTCPVOICE = new Channel("myrtcpvoice"); + + private String value; + + public Channel(String value) { + this.value = value; + } + + public String toString() { + return value; + } + + /** + * Returns the MediaChannel constant associated with the String value. + */ + public static Channel fromString(String value) { + if (value == null) { + return MYRTPVOICE; + } + value = value.toLowerCase(); + if (value.equals("myrtpvoice")) { + return MYRTPVOICE; + } else if (value.equals("tcp")) { + return MYRTCPVOICE; + } else { + return MYRTPVOICE; + } + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Channel other = (Channel) obj; + if (value == null) { + if (other.value != null) { + return false; + } + } else if (!value.equals(other.value)) { + return false; + } + return true; + } + + /** + * Return true if the channel is not valid. + * + * @return true if the channel is null + */ + public boolean isNull() { + if (value == null) { + return true; + } else if (value.length() == 0) { + return true; + } else { + return false; + } + } + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TransportNegotiator.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TransportNegotiator.java new file mode 100644 index 000000000..a76d0c3ad --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TransportNegotiator.java @@ -0,0 +1,842 @@ +package org.jivesoftware.smackx.jingle.nat; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.jingle.JingleNegotiator; +import org.jivesoftware.smackx.jingle.JingleSession; +import org.jivesoftware.smackx.jingle.listeners.JingleListener; +import org.jivesoftware.smackx.jingle.listeners.JingleTransportListener; +import org.jivesoftware.smackx.packet.Jingle; +import org.jivesoftware.smackx.packet.JingleTransport.JingleTransportCandidate; + +import java.io.IOException; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * Transport negotiator. + * + * + * This class is responsible for managing the transport negotiation process, + * handling all the packet interchange and the stage control. + * + * @author Alvaro Saurin + */ +public abstract class TransportNegotiator extends JingleNegotiator { + + // The time we give to the candidates check before we accept or decline the + // transport (in milliseconds) + private final static int CANDIDATES_ACCEPT_PERIOD = 4000; + + // The session this nenotiator belongs to + private final JingleSession session; + + // The transport manager + private final TransportResolver resolver; + + // Transport candidates we have offered + private final List offeredCandidates = new ArrayList(); + + // List of remote transport candidates + private final List remoteCandidates = new ArrayList(); + + // Valid remote candidates + private final List validRemoteCandidates = new ArrayList(); + + // The best local candidate we have offered (and accepted by the other part) + private TransportCandidate acceptedLocalCandidate; + + // The thread that will report the result to the other end + private Thread resultThread; + + // Listener for the resolver + private TransportResolverListener.Resolver resolverListener; + + // states + private final Inviting inviting; + + private final Accepting accepting; + + private final Pending pending; + + private final Active active; + + /** + * Default constructor. + * + * @param js The Jingle session + * @param transResolver The JingleTransportManager to use + */ + public TransportNegotiator(JingleSession js, + TransportResolver transResolver) { + super(js.getConnection()); + + session = js; + resolver = transResolver; + + resultThread = null; + + // Create the states... + inviting = new Inviting(this); + accepting = new Accepting(this); + pending = new Pending(this); + active = new Active(this); + } + + /** + * Get a new instance of the right TransportNegotiator class with this + * candidate. + * + * @return A TransportNegotiator instance + */ + public abstract org.jivesoftware.smackx.packet.JingleTransport getJingleTransport(TransportCandidate cand); + + /** + * Return true if the transport candidate is acceptable for the current + * negotiator. + * + * @return true if the transport candidate is acceptable + */ + public abstract boolean acceptableTransportCandidate(TransportCandidate tc); + + /** + * Obtain the best local candidate we want to offer. + * + * @return the best local candidate + */ + public final TransportCandidate getBestLocalCandidate() { + return resolver.getPreferredCandidate(); + } + + /** + * Set the best local transport candidate we have offered and accepted by + * the other endpoint. + * + * @param bestLocalCandidate the acceptedLocalCandidate to set + */ + private void setAcceptedLocalCandidate(TransportCandidate bestLocalCandidate) + throws XMPPException { + for (int i = 0; i < resolver.getCandidateCount(); i++) { + //TODO FIX The EQUAL Sentence + if (resolver.getCandidate(i).getIp().equals(bestLocalCandidate.getIp())) { + acceptedLocalCandidate = resolver.getCandidate(i); + return; + } + } + //System.out.println("BEST: " + bestLocalCandidate.getIp()); + throw new XMPPException("Local transport candidate has not be offered."); + } + + /** + * Get the best accepted local candidate we have offered. + * + * @return a transport candidate we have offered. + */ + public TransportCandidate getAcceptedLocalCandidate() { + return acceptedLocalCandidate; + } + + /** + * Obtain the best common transport candidate obtained in the negotiation. + * + * @return the bestRemoteCandidate + */ + public abstract TransportCandidate getBestRemoteCandidate(); + + /** + * Get the list of remote candidates. + * + * @return the remoteCandidates + */ + private List getRemoteCandidates() { + return remoteCandidates; + } + + /** + * Add a remote candidate to the list. The candidate will be checked in + * order to verify if it is usable. + * + * @param rc a remote candidate to add and check. + */ + private void addRemoteCandidate(TransportCandidate rc) { + // Add the candidate to the list + if (rc != null) { + if (acceptableTransportCandidate(rc)) { + synchronized (remoteCandidates) { + remoteCandidates.add(rc); + } + + // Check if the new candidate can be used. + checkRemoteCandidate(rc); + } + } + } + + /** + * Add a offered candidate to the list. + * + * @param rc a remote candidate we have offered. + */ + private void addOfferedCandidate(TransportCandidate rc) { + // Add the candidate to the list + if (rc != null) { + synchronized (offeredCandidates) { + offeredCandidates.add(rc); + } + } + } + + /** + * Check asynchronously the new transport candidate. + * + * @param offeredCandidate a transport candidates to check + */ + private void checkRemoteCandidate(final TransportCandidate offeredCandidate) { + System.out.println("CHECK"); + offeredCandidate.addListener(new TransportResolverListener.Checker() { + public void candidateChecked(TransportCandidate cand, + final boolean validCandidate) { + if (validCandidate) { + addValidRemoteCandidate(offeredCandidate); + } + } + + public void candidateChecking(TransportCandidate cand) { + } + + }); + offeredCandidate.check(); + } + + /** + * Return true if the transport is established. + * + * @return true if the transport is established. + */ + private boolean isEstablished() { + return getBestRemoteCandidate() != null && getAcceptedLocalCandidate() != null; + } + + /** + * Return true if the transport is fully established. + * + * @return true if the transport is fully established. + */ + public final boolean isFullyEstablished() { + return isEstablished() && getState() == active; + } + + /** + * Launch a thread that checks, after some time, if any of the candidates + * offered by the other endpoint is usable. The thread does not check the + * candidates: it just checks if we have got a valid one and sends an Accept + * in that case. + */ + private void delayedCheckBestCandidate(final JingleSession js, final Jingle jin) { + // + // If this is the first insertion in the list, start the thread that + // will send the result of our checks... + // + if (resultThread == null && !getRemoteCandidates().isEmpty()) { + resultThread = new Thread(new Runnable() { + + public void run() { + + // Sleep for some time, waiting for the candidates checks + try { + Thread.sleep(CANDIDATES_ACCEPT_PERIOD + + TransportResolver.CHECK_TIMEOUT); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + // Once we are in pending state, look for any valid remote + // candidate, and send an "accept" if we have one... + TransportCandidate bestRemote = getBestRemoteCandidate(); + State state = getState(); + + if (bestRemote != null && (state == pending || state == active)) { + // Accepting the remote candidate + Jingle jout = new Jingle(Jingle.Action.TRANSPORTACCEPT); + jout.addTransport(getJingleTransport(bestRemote)); + + // Send the packet + js.sendFormattedJingle(jin, jout); + + if (isEstablished()) { + setState(active); + } + } + } + }, "Waiting for all the transport candidates checks..."); + + resultThread.setName("Transport Resolver Result"); + resultThread.start(); + } + } + + /** + * Add a valid remote candidate to the list. The remote candidate has been + * checked, and the remote + * + * @param remoteCandidate a remote candidate to add + */ + private void addValidRemoteCandidate(TransportCandidate remoteCandidate) { + // Add the candidate to the list + if (remoteCandidate != null) { + synchronized (validRemoteCandidates) { + System.out.println("ADDED Valid Cand: " + remoteCandidate.getIp()); + validRemoteCandidates.add(remoteCandidate); + } + } + } + + /** + * Get the list of valid (ie, checked) remote candidates. + * + * @return The list of valid (ie, already checked) remote candidates. + */ + final ArrayList getValidRemoteCandidatesList() { + synchronized (validRemoteCandidates) { + return new ArrayList(validRemoteCandidates); + } + } + + /** + * Get an iterator for the list of valid (ie, checked) remote candidates. + * + * @return The iterator for the list of valid (ie, already checked) remote + * candidates. + */ + public final Iterator getValidRemoteCandidates() { + return Collections.unmodifiableList(getRemoteCandidates()).iterator(); + } + + /** + * Add an offered remote candidate. The transport candidate can be unusable: + * we must check if we can use it. + * + * @param rc the remote candidate to add. + */ + private void addRemoteCandidates(List rc) { + if (rc != null) { + System.out.println("SIZE OF LISTA: " + rc.size()); + if (rc.size() > 0) { + for (Object aRc : rc) { + addRemoteCandidate((TransportCandidate) aRc); + } + } + } + } + + /** + * Parse the list of transport candidates from a Jingle packet. + * + * @param jin The input jingle packet + */ + private static ArrayList obtainCandidatesList(Jingle jin) { + ArrayList result = new ArrayList(); + + if (jin != null) { + // Get the list of candidates from the packet + Iterator iTrans = jin.getTransports(); + while (iTrans.hasNext()) { + org.jivesoftware.smackx.packet.JingleTransport trans = (org.jivesoftware.smackx.packet.JingleTransport) iTrans.next(); + + System.out.println("LISTA SIZE: " + trans.getCandidatesCount()); + + Iterator iCand = trans.getCandidates(); + while (iCand.hasNext()) { + JingleTransportCandidate cand = (JingleTransportCandidate) iCand + .next(); + TransportCandidate transCand = cand.getMediaTransport(); + result.add(transCand); + } + } + } + + return result; + } + + private boolean isOfferStarted() { + return resolver.isResolving() || resolver.isResolved(); + } + + /** + * Send an offer for a transport candidate + * + * @param cand + */ + private synchronized void sendTransportCandidateOffer(TransportCandidate cand) { + if (!cand.isNull()) { + // Offer our new candidate... + addOfferedCandidate(cand); + session.sendFormattedJingle(new Jingle(getJingleTransport(cand))); + } + } + + /** + * Create a Jingle packet where we announce our transport candidates. + * + * @throws XMPPException + */ + private void sendTransportCandidatesOffer() throws XMPPException { + List notOffered = resolver.getCandidatesList(); + + notOffered.removeAll(offeredCandidates); + + // Send any unset candidate + for (Object aNotOffered : notOffered) { + sendTransportCandidateOffer((TransportCandidate) aNotOffered); + } + + // .. and start a listener that will send any future candidate + if (resolverListener == null) { + // Add a listener that sends the offer when the resolver finishes... + resolverListener = new TransportResolverListener.Resolver() { + public void candidateAdded(TransportCandidate cand) { + sendTransportCandidateOffer(cand); + } + + public void end() { + } + + public void init() { + } + }; + + resolver.addListener(resolverListener); + } + + if (!(resolver.isResolving() || resolver.isResolved())) { + // Resolve our IP and port + System.out.println("RESOLVER CALLED"); + resolver.resolve(); + } + } + + /** + * Dispatch an incoming packet. The method is responsible for recognizing + * the packet type and, depending on the current state, deliverying the + * packet to the right event handler and wait for a response. + * + * @param iq the packet received + * @return the new Jingle packet to send. + * @throws XMPPException + */ + public final IQ dispatchIncomingPacket(IQ iq, String id) throws XMPPException { + IQ jout = null; + + if (invalidState()) { + if (iq == null) { + // With a null packet, we are just inviting the other end... + setState(inviting); + jout = getState().eventInvite(); + + } else { + if (iq instanceof Jingle) { + // If there is no specific jmf action associated, then we + // are being invited to a new session... + setState(accepting); + jout = getState().eventInitiate((Jingle) iq); + } else { + throw new IllegalStateException( + "Invitation IQ received is not a Jingle packet in Transport negotiator."); + } + } + } else { + if (iq == null) { + return null; + } else { + if (iq.getType().equals(IQ.Type.ERROR)) { + // Process errors + getState().eventError(iq); + } else if (iq.getType().equals(IQ.Type.RESULT)) { + // Process ACKs + if (isExpectedId(iq.getPacketID())) { + jout = getState().eventAck(iq); + removeExpectedId(iq.getPacketID()); + } + } else if (iq instanceof Jingle) { + // Get the action from the Jingle packet + Jingle jin = (Jingle) iq; + Jingle.Action action = jin.getAction(); + + if (action != null) { + if (action.equals(Jingle.Action.TRANSPORTACCEPT)) { + jout = getState().eventAccept(jin); + } else if (action.equals(Jingle.Action.TRANSPORTDECLINE)) { + jout = getState().eventDecline(jin); + } else if (action.equals(Jingle.Action.TRANSPORTINFO)) { + jout = getState().eventInfo(jin); + } else if (action.equals(Jingle.Action.TRANSPORTMODIFY)) { + jout = getState().eventModify(jin); + } + } + } + } + } + + // Save the Id for any ACK + if (id != null) { + addExpectedId(id); + } else { + if (jout != null) { + addExpectedId(jout.getPacketID()); + } + } + + return jout; + } + + /** + * Trigger a Transport session established event. + * + * @param local TransportCandidate that has been agreed. + * @param remote TransportCandidate that has been agreed. + */ + private void triggerTransportEstablished(TransportCandidate local, + TransportCandidate remote) { + ArrayList listeners = getListenersList(); + for (Object listener : listeners) { + JingleListener li = (JingleListener) listener; + if (li instanceof JingleTransportListener) { + JingleTransportListener mli = (JingleTransportListener) li; + System.out.println("triggerTransportEstablished " + local.getLocalIp()); + mli.transportEstablished(local, remote); + } + } + } + + /** + * Trigger a Transport closed event. + * + * @param cand current TransportCandidate that is cancelled. + */ + private void triggerTransportClosed(TransportCandidate cand) { + ArrayList listeners = getListenersList(); + for (Object listener : listeners) { + JingleListener li = (JingleListener) listener; + if (li instanceof JingleTransportListener) { + JingleTransportListener mli = (JingleTransportListener) li; + mli.transportClosed(cand); + } + } + } + + // States + + /** + * First stage when we send a session request. + */ + public final class Inviting extends JingleNegotiator.State { + + public Inviting(TransportNegotiator neg) { + super(neg); + } + + /** + * Create an initial Jingle packet with an empty transport. + */ + public Jingle eventInvite() { + return new Jingle(getJingleTransport(null)); + } + + /** + * We have received some candidates. This can happen _before_ the ACK + * has been recieved... + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventInfo(org.jivesoftware.smackx.packet.Jingle) + */ + public Jingle eventInfo(Jingle jin) throws XMPPException { + // Parse the Jingle and get any proposed transport candidates + addRemoteCandidates(obtainCandidatesList(jin)); + + // Wait for some time and check if we have a valid candidate to + // use... + delayedCheckBestCandidate(session, jin); + + return null;// super.eventInfo(jin); + } + + /** + * The other endpoint has partially accepted our invitation: start + * offering a list of candidates. + * + * @return an IQ packet + * @throws XMPPException + */ + public Jingle eventAck(IQ iq) throws XMPPException { + sendTransportCandidatesOffer(); + setState(pending); + return super.eventAck(iq); + } + } + + /** + * We are accepting connections. This is the starting state when we accept a + * connection... + */ + public final class Accepting extends JingleNegotiator.State { + public Accepting(TransportNegotiator neg) { + super(neg); + } + + /** + * We have received an invitation. The packet will be ACKed by lower + * levels... + */ + public Jingle eventInitiate(Jingle jin) throws XMPPException { + // Parse the Jingle and get any proposed transport candidates + //addRemoteCandidates(obtainCandidatesList(jin)); + + // Start offering candidates + sendTransportCandidatesOffer(); + + // All these candidates will be checked asyncronously. Wait for some + // time and check if we have a valid candidate to use... + delayedCheckBestCandidate(session, jin); + + // Set the next state + setState(pending); + + return super.eventInitiate(jin); + } + } + + /** + * We are still receiving candidates + */ + public final class Pending extends JingleNegotiator.State { + + public Pending(TransportNegotiator neg) { + super(neg); + } + + /** + * One of our transport candidates has been accepted. + * + * @param jin The input packet + * @return a Jingle packet + * @throws XMPPException an exception + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAccept(org.jivesoftware.smackx.packet.Jingle) + */ + public Jingle eventAccept(Jingle jin) throws XMPPException { + Jingle response = null; + + // Parse the Jingle and get the accepted candidate + ArrayList accepted = obtainCandidatesList(jin); + if (!accepted.isEmpty()) { + + for (TransportCandidate cand : (List) accepted) { + System.out.println("Cand: " + cand.getIp()); + } + + TransportCandidate cand = (TransportCandidate) accepted.get(0); + setAcceptedLocalCandidate(cand); + + if (isEstablished()) { + + System.out.println("SET ACTIVE"); + + setState(active); + } + } + return response; + } + + /** + * We have received another remote transport candidates. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventInfo(org.jivesoftware.smackx.packet.Jingle) + */ + public Jingle eventInfo(Jingle jin) throws XMPPException { + + sendTransportCandidatesOffer(); + + // Parse the Jingle and get any proposed transport candidates + addRemoteCandidates(obtainCandidatesList(jin)); + + // Wait for some time and check if we have a valid candidate to + // use... + delayedCheckBestCandidate(session, jin); + + return null;//super.eventInfo(jin); + } + + /** + * None of our transport candidates has been accepted... + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventDecline(org.jivesoftware.smackx.packet.Jingle) + */ + public Jingle eventDecline(Jingle inJingle) throws JingleException { + throw new JingleException("No common payload found."); + } + } + + /** + * "Active" state: we have an agreement about the codec... + */ + public final class Active extends JingleNegotiator.State { + + public Active(TransportNegotiator neg) { + super(neg); + } + + /** + * We have an agreement. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter() + */ + public void eventEnter() { + System.out.println("Transport stabilished"); + triggerTransportEstablished(getAcceptedLocalCandidate(), + getBestRemoteCandidate()); + super.eventEnter(); + } + + /** + * We have finished the transport. + * + * @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter() + */ + public void eventExit() { + triggerTransportClosed(null); + super.eventExit(); + } + } + + // Subclasses + + /** + * Raw-UDP transport negotiator + * + * @author Alvaro Saurin + */ + public static final class RawUdp extends TransportNegotiator { + + /** + * Default constructor, with a JingleSession and transport manager. + * + * @param js The Jingle session this negotiation belongs to. + * @param res The transport resolver to use. + */ + public RawUdp(JingleSession js, final TransportResolver res) { + super(js, res); + } + + /** + * Get a TransportNegotiator instance. + */ + public org.jivesoftware.smackx.packet.JingleTransport getJingleTransport(TransportCandidate bestRemote) { + org.jivesoftware.smackx.packet.JingleTransport.RawUdp jt = new org.jivesoftware.smackx.packet.JingleTransport.RawUdp(); + jt.addCandidate(new org.jivesoftware.smackx.packet.JingleTransport.RawUdp.Candidate(bestRemote)); + return jt; + } + + /** + * Obtain the best common transport candidate obtained in the + * negotiation. + * + * @return the bestRemoteCandidate + */ + public TransportCandidate getBestRemoteCandidate() { + // Hopefully, we only have one validRemoteCandidate + ArrayList cands = getValidRemoteCandidatesList(); + if (!cands.isEmpty()) { + return (TransportCandidate) cands.get(0); + } else { + System.out.println("No Remote Candidate"); + return null; + } + } + + /** + * Return true for fixed candidates. + */ + public boolean acceptableTransportCandidate(TransportCandidate tc) { + return tc instanceof TransportCandidate.Fixed; + } + } + + /** + * Ice transport negotiator. + * + * @author Alvaro Saurin + */ + public static final class Ice extends TransportNegotiator { + + /** + * Default constructor, with a JingleSession and transport manager. + * + * @param js The Jingle session this negotiation belongs to. + * @param res The transport manager to use. + */ + public Ice(JingleSession js, final TransportResolver res) { + super(js, res); + } + + /** + * Get a TransportNegotiator instance. + * + * @param candidate + */ + public org.jivesoftware.smackx.packet.JingleTransport getJingleTransport(TransportCandidate candidate) { + org.jivesoftware.smackx.packet.JingleTransport.Ice jt = new org.jivesoftware.smackx.packet.JingleTransport.Ice(); + jt.addCandidate(new org.jivesoftware.smackx.packet.JingleTransport.Ice.Candidate(candidate)); + return jt; + } + + /** + * Obtain the best remote candidate obtained in the negotiation so far. + * + * @return the bestRemoteCandidate + */ + public TransportCandidate getBestRemoteCandidate() { + TransportCandidate.Ice result = null; + + ArrayList cands = getValidRemoteCandidatesList(); + if (!cands.isEmpty()) { + int lowest = 65560; + TransportCandidate.Ice chose = null; + for (TransportCandidate.Ice transportCandidate : cands) { + System.out.println("Pref: " + transportCandidate.getPreference() + " :" + transportCandidate.getIp()); + if (transportCandidate.getPreference() < lowest) { + chose = transportCandidate; + lowest = transportCandidate.getPreference(); + } + } + result = chose; + } + + return result; + } + + /** + * Return true for ICE candidates. + */ + public boolean acceptableTransportCandidate(TransportCandidate tc) { + + try { + InetAddress.getByName(tc.getIp()).isReachable(3000); + DatagramSocket socket = new DatagramSocket(0); + socket.connect(InetAddress.getByName(tc.getIp()), tc.getPort()); + return true; + } catch (SocketException e) { + e.printStackTrace(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + return false; + } + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TransportResolver.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TransportResolver.java new file mode 100644 index 000000000..3a99d8e6e --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TransportResolver.java @@ -0,0 +1,433 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright (C) 2002-2006 Jive Software. All rights reserved. + * ==================================================================== + * The Jive Software License (based on Apache Software License, Version 1.1) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by + * Jive Software (http://www.jivesoftware.com)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Smack" and "Jive Software" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please + * contact webmaster@jivesoftware.com. + * + * 5. Products derived from this software may not be called "Smack", + * nor may "Smack" appear in their name, without prior written + * permission of Jive Software. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL JIVE SOFTWARE OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + */ + +package org.jivesoftware.smackx.jingle.nat; + +import org.jivesoftware.smack.XMPPException; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * A TransportResolver is used for obtaining a list of valid transport + * candidates. A transport candidate is composed by an IP address and a port number. + * It is called candidate, because it can be elected or not. + * + * @author Thiago Camargo + * @author Alvaro Saurin + */ +public abstract class TransportResolver { + + public enum Type { + rawupd, ice + } + + ; + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public Type type = Type.rawupd; + + // the time, in milliseconds, before a check aborts + public static final int CHECK_TIMEOUT = 3000; + + // Listeners for events + private final ArrayList listeners = new ArrayList(); + + // TRue if the resolver is working + private boolean resolving; + + // This will be true when all the transport candidates have been gathered... + private boolean resolved; + + // This indicates that a transport resolver is initialized + private boolean initialized = false; + + // We store a list of candidates internally, just in case there are several + // possibilities. When the user asks for a transport, we return the best + // one. + protected final List candidates = new ArrayList(); + + // Remote candidates that are being checked + private final static ArrayList candidatesChecking = new ArrayList(); + + /** + * Default constructor. + */ + protected TransportResolver() { + super(); + + resolving = false; + resolved = false; + } + + /** + * Initialize the Resolver + */ + public abstract void initialize() throws XMPPException; + + /** + * Start a the resolution. + */ + public abstract void resolve() throws XMPPException; + + /** + * Clear the list of candidates and start a new resolution process. + * + * @throws XMPPException + */ + public void clear() throws XMPPException { + cancel(); + candidates.clear(); + //resolve(); + } + + /** + * Cancel any asynchronous resolution operation. + */ + public abstract void cancel() throws XMPPException; + + /** + * Return true if the resolver is working. + * + * @return true if the resolver is working. + */ + public boolean isResolving() { + return resolving; + } + + /** + * Return true if the resolver has finished the search for transport + * candidates. + * + * @return true if the search has finished + */ + public boolean isResolved() { + return resolved; + } + + /** + * Set the Transport Resolver as initialized. + */ + public synchronized void setInitialized() { + initialized = true; + } + + /** + * Chack if the Transport Resolver is initialized + * + * @return + */ + public synchronized boolean isInitialized() { + return initialized; + } + + /** + * Indicate the beggining of the resolution process. This method must be + * used by subclasses at the begining of their resolve() method. + */ + protected synchronized void setResolveInit() { + resolved = false; + resolving = true; + + triggerResolveInit(); + } + + /** + * Indicate the end of the resolution process. This method must be used by + * subclasses at the begining of their resolve() method. + */ + protected synchronized void setResolveEnd() { + resolved = true; + resolving = false; + + triggerResolveEnd(); + } + + // Listeners management + + /** + * Add a transport resolver listener. + * + * @param li The transport resolver listener to be added. + */ + public void addListener(TransportResolverListener li) { + synchronized (listeners) { + listeners.add(li); + } + } + + /** + * Removes a transport resolver listener. + * + * @param li The transport resolver listener to be removed + */ + public void removeListener(TransportResolverListener li) { + synchronized (listeners) { + listeners.remove(li); + } + } + + /** + * Get the list of listeners + * + * @return the list of listeners + */ + public ArrayList getListenersList() { + synchronized (listeners) { + return new ArrayList(listeners); + } + } + + /** + * Trigger a new candidate added event. + * + * @param cand The candidate added to the list of candidates. + */ + protected void triggerCandidateAdded(TransportCandidate cand) { + Iterator iter = getListenersList().iterator(); + while (iter.hasNext()) { + TransportResolverListener trl = (TransportResolverListener) iter.next(); + if (trl instanceof TransportResolverListener.Resolver) { + TransportResolverListener.Resolver li = (TransportResolverListener.Resolver) trl; + System.out.println("triggerCandidateAdded : " + cand.getLocalIp()); + li.candidateAdded(cand); + } + } + } + + /** + * Trigger a event notifying the initialization of the resolution process. + */ + private void triggerResolveInit() { + Iterator iter = getListenersList().iterator(); + while (iter.hasNext()) { + TransportResolverListener trl = (TransportResolverListener) iter.next(); + if (trl instanceof TransportResolverListener.Resolver) { + TransportResolverListener.Resolver li = (TransportResolverListener.Resolver) trl; + li.init(); + } + } + } + + /** + * Trigger a event notifying the obtention of all the candidates. + */ + private void triggerResolveEnd() { + Iterator iter = getListenersList().iterator(); + while (iter.hasNext()) { + TransportResolverListener trl = (TransportResolverListener) iter.next(); + if (trl instanceof TransportResolverListener.Resolver) { + TransportResolverListener.Resolver li = (TransportResolverListener.Resolver) trl; + li.end(); + } + } + } + + // Candidates management + + /** + * Clear the list of candidate + */ + protected void clearCandidates() { + synchronized (candidates) { + candidates.clear(); + } + } + + /** + * Add a new transport candidate + * + * @param cand The candidate to add + */ + protected void addCandidate(TransportCandidate cand) { + synchronized (candidates) { + if (!candidates.contains(cand)) + candidates.add(cand); + } + + // Notify the listeners + triggerCandidateAdded(cand); + } + + /** + * Get an iterator for the list of candidates + * + * @return an iterator + */ + public Iterator getCandidates() { + synchronized (candidates) { + System.out.println("CNUM: " + candidates.size()); + return Collections.unmodifiableList(new ArrayList(candidates)).iterator(); + } + } + + /** + * Get the candididate with the highest preference. + * + * @return The best candidate, according to the preference order. + */ + public TransportCandidate getPreferredCandidate() { + TransportCandidate result = null; + + ArrayList cands = (ArrayList) getCandidatesList(); + if (cands.size() > 0) { + Collections.sort(cands); + // Return the last candidate + result = (TransportCandidate) cands.get(cands.size() - 1); + System.out.println("Result: " + result.getIp()); + } + + return result; + } + + /** + * Get the numer of transport candidates. + * + * @return The length of the transport candidates list. + */ + public int getCandidateCount() { + synchronized (candidates) { + return candidates.size(); + } + } + + /** + * Get the list of candidates + * + * @return the list of transport candidates + */ + public List getCandidatesList() { + ArrayList result = null; + + synchronized (candidates) { + result = new ArrayList(candidates); + } + + return result; + } + + /** + * Get the n-th candidate + * + * @return a transport candidate + */ + public TransportCandidate getCandidate(int i) { + TransportCandidate cand; + + synchronized (candidates) { + cand = (TransportCandidate) candidates.get(i); + } + return cand; + } + + /** + * Initialize Transport Resolver and wait until it is complete unitialized. + */ + public void initializeAndWait() throws XMPPException { + this.initialize(); + try { + System.out.print("Initializing..."); + while (!this.isInitialized()) { + System.out.print("."); + Thread.sleep(1000); + } + System.out.print("Resolved\n"); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Obtain a free port we can use. + * + * @return A free port number. + */ + protected int getFreePort() { + ServerSocket ss; + int freePort = 0; + + for (int i = 0; i < 10; i++) { + freePort = (int) (10000 + Math.round(Math.random() * 10000)); + freePort = freePort % 2 == 0 ? freePort : freePort + 1; + try { + ss = new ServerSocket(freePort); + freePort = ss.getLocalPort(); + ss.close(); + return freePort; + } + catch (IOException e) { + e.printStackTrace(); + } + } + try { + ss = new ServerSocket(0); + freePort = ss.getLocalPort(); + ss.close(); + } + catch (IOException e) { + e.printStackTrace(); + } + return freePort; + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TransportResolverListener.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TransportResolverListener.java new file mode 100644 index 000000000..d75ca7a69 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TransportResolverListener.java @@ -0,0 +1,48 @@ +package org.jivesoftware.smackx.jingle.nat; + +/** + * Transport resolver Interface + */ +public abstract interface TransportResolverListener { + /** + * Resolver listener. + */ + public interface Resolver extends TransportResolverListener { + /** + * The resolution process has been started. + */ + public void init(); + + /** + * A transport candidate has been added + * + * @param cand The transport candidate. + */ + public void candidateAdded(TransportCandidate cand); + + /** + * All the transport candidates have been obtained. + */ + public void end(); + } + + /** + * Resolver checker. + */ + public interface Checker extends TransportResolverListener { + /** + * A transport candidate has been checked. + * + * @param cand The transport candidate that has been checked. + * @param result True if the candidate is usable. + */ + public void candidateChecked(TransportCandidate cand, boolean result); + + /** + * A transport candidate is being checked. + * + * @param cand The transport candidate that is being checked. + */ + public void candidateChecking(TransportCandidate cand); + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/package.html b/jingle/extension/source/org/jivesoftware/smackx/package.html new file mode 100644 index 000000000..d574a2a4d --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/package.html @@ -0,0 +1 @@ +Smack extensions API. \ No newline at end of file diff --git a/jingle/extension/source/org/jivesoftware/smackx/packet/Jingle.java b/jingle/extension/source/org/jivesoftware/smackx/packet/Jingle.java new file mode 100644 index 000000000..63280d9bf --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/packet/Jingle.java @@ -0,0 +1,504 @@ +/** + * $RCSfile$ + * $Revision: 2407 $ + * $Date: 2004-11-02 23:37:00 +0000 (Tue, 02 Nov 2004) $ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.packet.IQ; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * An Jingle sub-packet, which is used by XMPP clients to exchange info like + * descriptions and transports.

The following link summarizes the + * requirements of Jingle IM: Valid tags. + *

+ *

Warning: this is an non-standard protocol documented by JEP-166. Because this is + * a non-standard protocol, it is subject to change. + * + * @author Alvaro Saurin + */ +public class Jingle extends IQ { + + // static + + public static final String NAMESPACE = "http://jabber.org/protocol/jingle"; + + public static final String NODENAME = "jingle"; + + // non-static + + private String sid; // The session id + + private Action action; // The action associated to the Jingle + + private String initiator; // The initiator as a "user@host/resource" + + private String responder; // The responder + + // Sub-elements of a Jingle object. + + private final List descriptions = new ArrayList(); + + private final List transports = new ArrayList(); + + private JingleContentInfo contentInfo; + + /** + * A constructor where the main components can be initialized. + */ + public Jingle(final List descs, final List trans, final JingleContentInfo mi, + final String sid) { + super(); + + if (descs != null) { + descriptions.addAll(descs); + } + + if (trans != null) { + transports.addAll(trans); + } + + setContentInfo(mi); + setSid(sid); + + // Set null all other fields in the packet + initiator = null; + responder = null; + action = null; + } + + /** + * Constructor with a description. + * + * @param descr a description + */ + public Jingle(final JingleContentDescription descr) { + super(); + + addDescription(descr); + + // Set null all other fields in the packet + initiator = null; + responder = null; + + // Some default values for the most common situation... + action = Jingle.Action.DESCRIPTIONINFO; + this.setType(IQ.Type.SET); + } + + /** + * Constructor with a transport. + * + * @param trans a transport + */ + public Jingle(final JingleTransport trans) { + super(); + + addTransport(trans); + + // Set null all other fields in the packet + initiator = null; + responder = null; + + // Some default values for the most common situation... + action = Jingle.Action.TRANSPORTINFO; + this.setType(IQ.Type.SET); + } + + /** + * Constructor with a content info. + * + * @param info The content info + */ + public Jingle(final JingleContentInfo info) { + super(); + + setContentInfo(info); + + // Set null all other fields in the packet + initiator = null; + responder = null; + + // Some default values for the most common situation... + action = Jingle.Action.DESCRIPTIONINFO; + this.setType(IQ.Type.SET); + } + + /** + * A constructor where the action can be specified. + * + * @param action The action. + */ + public Jingle(final Jingle.Action action) { + this(null, null, null, null); + this.action = action; + + // In general, a Jingle with an action is used in a SET packet... + this.setType(IQ.Type.SET); + } + + /** + * A constructor where the session ID can be specified. + * + * @param sid The session ID related to the negotiation. + * @see #setSid(String) + */ + public Jingle(final String sid) { + this(null, null, null, sid); + } + + /** + * The default constructor + */ + public Jingle() { + super(); + } + + /** + * Set the session ID related to this session. The session ID is a unique + * identifier generated by the initiator. This should match the XML Nmtoken + * production so that XML character escaping is not needed for characters + * such as &. + * + * @param sid the session ID + */ + public final void setSid(final String sid) { + this.sid = sid; + } + + /** + * Returns the session ID related to the session. The session ID is a unique + * identifier generated by the initiator. This should match the XML Nmtoken + * production so that XML character escaping is not needed for characters + * such as &. + * + * @return Returns the session ID related to the session. + * @see #setSid(String) + */ + public String getSid() { + + return sid; + } + + /** + * Returns the XML element name of the extension sub-packet root element. + * Always returns "jingle" + * + * @return the XML element name of the packet extension. + */ + public static String getElementName() { + return NODENAME; + } + + /** + * Returns the XML namespace of the extension sub-packet root element. + * According the specification the namespace is always + * "http://jabber.org/protocol/jingle" + * + * @return the XML namespace of the packet extension. + */ + public static String getNamespace() { + return NAMESPACE; + } + + /** + * @return the audioInfo + */ + public JingleContentInfo getContentInfo() { + return contentInfo; + } + + /** + * @param contentInfo the audioInfo to set + */ + public void setContentInfo(final JingleContentInfo contentInfo) { + this.contentInfo = contentInfo; + } + + /** + * Get an iterator for the content descriptions + * + * @return the descriptions + */ + public Iterator getDescriptions() { + synchronized (descriptions) { + return Collections.unmodifiableList(new ArrayList(descriptions)).iterator(); + } + } + + /** + * Get an iterator for the content descriptions + * + * @return the descriptions + */ + public ArrayList getDescriptionsList() { + synchronized (descriptions) { + return new ArrayList(descriptions); + } + } + + /** + * Add a new content description. + * + * @param desc the descriptions to add + */ + public void addDescription(final JingleContentDescription desc) { + if (desc != null) { + synchronized (descriptions) { + descriptions.add(desc); + } + } + } + + /** + * Add a list of JingleContentDescription elements + * + * @param descsList the list of transports to add + */ + public void addDescriptions(final List descsList) { + if (descsList != null) { + synchronized (descriptions) { + descriptions.addAll(descsList); + } + } + } + + /** + * Get an iterator for the transport. + * + * @return the transports + */ + public Iterator getTransports() { + synchronized (transports) { + return Collections.unmodifiableList(new ArrayList(transports)).iterator(); + } + } + + /** + * Get the list of transports. + * + * @return the transports list. + */ + public ArrayList getTransportsList() { + synchronized (transports) { + return new ArrayList(transports); + } + } + + /** + * Add a new TransportNegotiator element + * + * @param trans the transports to add + */ + public void addTransport(final JingleTransport trans) { + if (trans != null) { + synchronized (transports) { + transports.add(trans); + } + } + } + + /** + * Add a list of TransportNegotiator elements + * + * @param transList the list of transports to add + */ + public void addTransports(final List transList) { + if (transList != null) { + synchronized (transports) { + transports.addAll(transList); + } + } + } + + /** + * Get the action specified in the packet + * + * @return the action + */ + public Action getAction() { + return action; + } + + /** + * Set the action in the packet + * + * @param action the action to set + */ + public void setAction(final Action action) { + this.action = action; + } + + /** + * Get the initiator. The initiator will be the full JID of the entity that + * has initiated the flow (which may be different to the "from" address in + * the IQ) + * + * @return the initiator + */ + public String getInitiator() { + return initiator; + } + + /** + * Set the initiator. The initiator must be the full JID of the entity that + * has initiated the flow (which may be different to the "from" address in + * the IQ) + * + * @param initiator the initiator to set + */ + public void setInitiator(final String initiator) { + this.initiator = initiator; + } + + /** + * Get the responder. The responder is the full JID of the entity that has + * replied to the initiation (which may be different to the "to" addresss in + * the IQ). + * + * @return the responder + */ + public String getResponder() { + return responder; + } + + /** + * Set the responder. The responder must be the full JID of the entity that + * has replied to the initiation (which may be different to the "to" + * addresss in the IQ). + * + * @param resp the responder to set + */ + public void setResponder(final String resp) { + responder = resp; + } + + /** + * Get a hash key for the session this packet belongs to. + * + * @param sid The session id + * @param initiator The initiator + * @return A hash key + */ + public static int getSessionHash(final String sid, final String initiator) { + final int PRIME = 31; + int result = 1; + result = PRIME * result + (initiator == null ? 0 : initiator.hashCode()); + result = PRIME * result + (sid == null ? 0 : sid.hashCode()); + return result; + } + + /** + * Return the XML representation of the packet. + * + * @return the XML string + */ + public String getChildElementXML() { + StringBuilder buf = new StringBuilder(); + + buf.append("<").append(getElementName()); + buf.append(" xmlns=\"").append(getNamespace()).append("\""); + if (getInitiator() != null) { + buf.append(" initiator=\"").append(getInitiator()).append("\""); + } + if (getResponder() != null) { + buf.append(" responder=\"").append(getResponder()).append("\""); + } + if (getAction() != null) { + buf.append(" action=\"").append(getAction()).append("\""); + } + if (getSid() != null) { + buf.append(" sid=\"").append(getSid()).append("\""); + } + buf.append(">"); + //TODO Update to accept more than one content per session (XEP-0166) + + buf.append(""); + // Look for possible payload types, and dump them. + synchronized (descriptions) { + for (int i = 0; i < descriptions.size(); i++) { + JingleContentDescription desc = (JingleContentDescription) descriptions + .get(i); + buf.append(desc.toXML()); + } + } + + // If the packet has transports, dump them. + synchronized (transports) { + for (int i = 0; i < transports.size(); i++) { + JingleTransport trans = (JingleTransport) transports.get(i); + buf.append(trans.toXML()); + } + } + buf.append(""); + + // and the same for audio jmf info + if (contentInfo != null) { + buf.append(contentInfo.toXML()); + } + + buf.append(""); + return buf.toString(); + } + + /** + * The "action" in the jingle packet, as an enum. + */ + public static enum Action { + + CONTENTACCEPT, CONTENTADD, CONTENTDECLINE, CONTENTMODIFY, + CONTENTREMOVE, DESCRIPTIONADD, DESCRIPTIONDECLINE, + DESCRIPTIONINFO, DESCRIPTIONMODIFY, SESSIONACCEPT, + SESSIONINFO, SESSIONINITIATE, SESSIONREDIRECT, + SESSIONTERMINATE, TRANSPORTACCEPT, TRANSPORTDECLINE, + TRANSPORTINFO, TRANSPORTMODIFY; + + private static String names[] = {"content-accept", "content-add", "content-decline", "content-modify", + "content-remove", "description-accept", "description-decline", "description-info", + "description-modify", "session-accept", "session-info", "session-initiate", + "session-redirect", "session-terminate", "transport-accept", "transport-decline", + "transport-info", "transport-modify"}; + + /** + * Returns the String value for an Action. + */ + + public String toString() { + return names[this.ordinal()]; + } + + /** + * Returns the Action for a String value. + */ + public static Action getAction(String str) { + for (int i = 0; i < names.length; i++) { + if (names[i].equals(str)) return Action.values()[i]; + } + return null; + } + + } + +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/packet/JingleContentDescription.java b/jingle/extension/source/org/jivesoftware/smackx/packet/JingleContentDescription.java new file mode 100644 index 000000000..c82df9017 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/packet/JingleContentDescription.java @@ -0,0 +1,278 @@ +package org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smackx.jingle.media.PayloadType; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * Jingle content description. + * + * @author Alvaro Saurin + */ +public abstract class JingleContentDescription implements PacketExtension { + + // static + + public static final String NODENAME = "description"; + + // non-static + + private final List payloads = new ArrayList(); + + /** + * Creates a content description.. + */ + public JingleContentDescription() { + super(); + } + + /** + * Returns the XML element name of the element. + * + * @return the XML element name of the element. + */ + public String getElementName() { + return NODENAME; + } + + /** + * Return the namespace. + * + * @return The namespace + */ + public abstract String getNamespace(); + + /** + * Adds a audio payload type to the packet. + * + * @param pt the audio payload type to add. + */ + public void addJinglePayloadType(final JinglePayloadType pt) { + synchronized (payloads) { + payloads.add(pt); + } + } + + /** + * Adds a list of payloads to the packet. + * + * @param pts the payloads to add. + */ + public void addAudioPayloadTypes(final List pts) { + synchronized (payloads) { + Iterator ptIter = pts.iterator(); + while (ptIter.hasNext()) { + PayloadType.Audio pt = (PayloadType.Audio) ptIter.next(); + addJinglePayloadType(new JinglePayloadType.Audio(pt)); + } + } + } + + /** + * Returns an Iterator for the audio payloads in the packet. + * + * @return an Iterator for the audio payloads in the packet. + */ + public Iterator getJinglePayloadTypes() { + return Collections.unmodifiableList(getJinglePayloadTypesList()).iterator(); + } + + /** + * Returns a list for the audio payloads in the packet. + * + * @return a list for the audio payloads in the packet. + */ + public ArrayList getJinglePayloadTypesList() { + synchronized (payloads) { + return new ArrayList(payloads); + } + } + + /** + * Return the list of Payload types contained in the description. + * + * @return a list of PayloadType.Audio + */ + public ArrayList getAudioPayloadTypesList() { + ArrayList result = new ArrayList(); + Iterator jinglePtsIter = getJinglePayloadTypes(); + + while (jinglePtsIter.hasNext()) { + JinglePayloadType jpt = (JinglePayloadType) jinglePtsIter.next(); + if (jpt instanceof JinglePayloadType.Audio) { + JinglePayloadType.Audio jpta = (JinglePayloadType.Audio) jpt; + result.add(jpta.getPayloadType()); + } + } + + return result; + } + + /** + * Returns a count of the audio payloads in the Jingle packet. + * + * @return the number of audio payloads in the Jingle packet. + */ + public int getJinglePayloadTypesCount() { + synchronized (payloads) { + return payloads.size(); + } + } + + /** + * Convert a Jingle description to XML. + * + * @return a string with the XML representation + */ + public String toXML() { + StringBuilder buf = new StringBuilder(); + + synchronized (payloads) { + if (payloads.size() > 0) { + buf.append("<").append(getElementName()); + buf.append(" xmlns=\"").append(getNamespace()).append("\" >"); + + Iterator pt = payloads.listIterator(); + while (pt.hasNext()) { + JinglePayloadType pte = (JinglePayloadType) pt.next(); + buf.append(pte.toXML()); + } + buf.append(""); + } + } + + return buf.toString(); + } + + /** + * Jingle audio description + */ + public static class Audio extends JingleContentDescription { + + public static final String NAMESPACE = "http://jabber.org/protocol/jingle/description/audio"; + + public Audio() { + super(); + } + + /** + * Utility constructor, with a JinglePayloadType + */ + public Audio(final JinglePayloadType pt) { + super(); + addJinglePayloadType(pt); + } + + public String getNamespace() { + return NAMESPACE; + } + } + + /** + * A payload type, contained in a descriptor. + * + * @author Alvaro Saurin + */ + public static class JinglePayloadType { + + public static final String NODENAME = "payload-type"; + + private PayloadType payload; + + /** + * Create a payload type. + * + * @param payload the payload + */ + public JinglePayloadType(final PayloadType payload) { + super(); + this.payload = payload; + } + + /** + * Create an empty payload type. + */ + public JinglePayloadType() { + this(null); + } + + /** + * Returns the XML element name of the element. + * + * @return the XML element name of the element. + */ + public static String getElementName() { + return NODENAME; + } + + /** + * Get the payload represented. + * + * @return the payload + */ + public PayloadType getPayloadType() { + return payload; + } + + /** + * Set the payload represented. + * + * @param payload the payload to set + */ + public void setPayload(final PayloadType payload) { + this.payload = payload; + } + + protected String getChildAttributes() { + return null; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + + if (payload != null) { + buf.append("<").append(getElementName()).append(" "); + + // We covert here the payload type to XML + if (payload.getId() != PayloadType.INVALID_PT) { + buf.append(" id=\"").append(payload.getId()).append("\""); + } + if (payload.getName() != null) { + buf.append(" name=\"").append(payload.getName()).append("\""); + } + if (payload.getChannels() != 0) { + buf.append(" channels=\"").append(payload.getChannels()).append("\""); + } + if (getChildAttributes() != null) { + buf.append(getChildAttributes()); + } + buf.append("/>"); + } + return buf.toString(); + } + + /** + * Audio payload type element + */ + public static class Audio extends JinglePayloadType { + public Audio(final PayloadType.Audio audio) { + super(audio); + } + + protected String getChildAttributes() { + StringBuilder buf = new StringBuilder(); + PayloadType pt = getPayloadType(); + if (pt instanceof PayloadType.Audio) { + PayloadType.Audio pta = (PayloadType.Audio) pt; + + buf.append(" clockrate=\"").append(pta.getClockRate()).append("\" "); + } + return buf.toString(); + } + } + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/packet/JingleContentInfo.java b/jingle/extension/source/org/jivesoftware/smackx/packet/JingleContentInfo.java new file mode 100644 index 000000000..66604f2b7 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/packet/JingleContentInfo.java @@ -0,0 +1,138 @@ +package org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smackx.jingle.media.ContentInfo; + +/** + * Jingle content info + * + * @author Alvaro Saurin + */ +public class JingleContentInfo implements PacketExtension { + + protected ContentInfo mediaInfoElement; + + private String namespace; + + /** + * Empty constructor, with no jmf info. + */ + public JingleContentInfo() { + this(null); + } + + /** + * Constructor with a jmf info + * + * @param mediaInfoElement MediaInfo element + */ + public JingleContentInfo(final ContentInfo mediaInfoElement) { + super(); + this.mediaInfoElement = mediaInfoElement; + } + + /** + * Get the jmf info element. + * + * @return the mediaInfoElement + */ + public ContentInfo getMediaInfo() { + return mediaInfoElement; + } + + /** + * Get the element name + */ + public String getElementName() { + // Media info is supposed to be just a single-word command... + return getMediaInfo().toString(); + } + + /** + * Set the name space. + * + * @param ns the namespace + */ + protected void setNamespace(final String ns) { + namespace = ns; + } + + /** + * Get the publilc namespace + */ + public String getNamespace() { + return namespace; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<").append(getElementName()).append(" xmlns=\""); + buf.append(getNamespace()).append("\" "); + buf.append("/>"); + return buf.toString(); + } + + /** + * Transport part of a Jingle packet. + */ + public static class Audio extends JingleContentInfo { + + public static final String NAMESPACE = "http://jabber.org/protocol/jingle/info/audio"; + + public Audio(final ContentInfo mi) { + super(mi); + setNamespace(NAMESPACE); + } + + public String getNamespace() { + return NAMESPACE; + } + + // Subclasses: specialize the Audio jmf info... + + /** + * Busy jmf info. + */ + public static class Busy extends Audio { + public Busy() { + super(ContentInfo.Audio.BUSY); + } + } + + /** + * Hold jmf info. + */ + public static class Hold extends Audio { + public Hold() { + super(ContentInfo.Audio.HOLD); + } + } + + /** + * Mute jmf info. + */ + public static class Mute extends Audio { + public Mute() { + super(ContentInfo.Audio.MUTE); + } + } + + /** + * Queued jmf info. + */ + public static class Queued extends Audio { + public Queued() { + super(ContentInfo.Audio.QUEUED); + } + } + + /** + * Ringing jmf info. + */ + public static class Ringing extends Audio { + public Ringing() { + super(ContentInfo.Audio.RINGING); + } + } + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/packet/JingleError.java b/jingle/extension/source/org/jivesoftware/smackx/packet/JingleError.java new file mode 100644 index 000000000..3d9d37e88 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/packet/JingleError.java @@ -0,0 +1,102 @@ +package org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.packet.PacketExtension; + +public class JingleError implements PacketExtension { + + public static String NAMESPACE = "http://jabber.org/protocol/jingle#error"; + + public static final JingleError OUT_OF_ORDER = new JingleError("out-of-order"); + + public static final JingleError UNKNOWN_SESSION = new JingleError("unknown-session"); + + public static final JingleError UNSUPPORTED_CONTENT = new JingleError( + "unsupported-content"); + + public static final JingleError UNSUPPORTED_TRANSPORTS = new JingleError( + "unsupported-transports"); + + // Non standard error messages + + public static final JingleError NO_COMMON_PAYLOAD = new JingleError( + "no-common-payload"); + + public static final JingleError NEGOTIATION_ERROR = new JingleError( + "negotiation-error"); + + public static final JingleError MALFORMED_STANZA = new JingleError("malformed-stanza"); + + private String message; + + /** + * Creates a new error with the specified code and message. + * + * @param message a message describing the error. + */ + public JingleError(final String message) { + this.message = message; + } + + /** + * Returns the message describing the error, or null if there is no message. + * + * @return the message describing the error, or null if there is no message. + */ + public String getMessage() { + return message; + } + + /** + * Returns the error as XML. + * + * @return the error as XML. + */ + public String toXML() { + StringBuilder buf = new StringBuilder(); + if (message != null) { + buf.append(""); + buf.append("<").append(message).append(" xmlns=\"").append(NAMESPACE).append( + "\"/>"); + buf.append(""); + } + return buf.toString(); + } + + /** + * Returns a Action instance associated with the String value. + */ + public static JingleError fromString(String value) { + if (value != null) { + value = value.toLowerCase(); + if (value.equals("out-of-order")) { + return OUT_OF_ORDER; + } else if (value.equals("unknown-session")) { + return UNKNOWN_SESSION; + } else if (value.equals("unsupported-content")) { + return UNSUPPORTED_CONTENT; + } else if (value.equals("unsupported-transports")) { + return UNSUPPORTED_TRANSPORTS; + } else if (value.equals("no-common-payload")) { + return NO_COMMON_PAYLOAD; + } else if (value.equals("negotiation-error")) { + return NEGOTIATION_ERROR; + } else if (value.equals("malformed-stanza")) { + return MALFORMED_STANZA; + } + + } + return null; + } + + public String toString() { + return getMessage(); + } + + public String getElementName() { + return message; + } + + public String getNamespace() { + return NAMESPACE; + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/packet/JingleTransport.java b/jingle/extension/source/org/jivesoftware/smackx/packet/JingleTransport.java new file mode 100644 index 000000000..6e0c6c520 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/packet/JingleTransport.java @@ -0,0 +1,409 @@ +package org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smackx.jingle.nat.TransportCandidate; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * A jingle transport extension + * + * @author Alvaro Saurin + */ +public class JingleTransport implements PacketExtension { + + // static + + public static final String NODENAME = "transport"; + + // non-static + + protected String namespace; + + protected final List candidates = new ArrayList(); + + /** + * Default constructor. + */ + public JingleTransport() { + super(); + } + + /** + * Utility constructor, with a transport candidate element. + * + * @param candidate A transport candidate element to add. + */ + public JingleTransport(final JingleTransportCandidate candidate) { + super(); + addCandidate(candidate); + } + + /** + * Copy constructor. + * + * @param tr the other jingle transport. + */ + public JingleTransport(final JingleTransport tr) { + if (tr != null) { + namespace = tr.namespace; + + if (tr.candidates.size() > 0) { + candidates.addAll(tr.candidates); + } + } + } + + /** + * Adds a transport candidate. + * + * @param candidate the candidate + */ + public void addCandidate(final JingleTransportCandidate candidate) { + if (candidate != null) { + synchronized (candidates) { + candidates.add(candidate); + } + } + } + + /** + * Get an iterator for the candidates + * + * @return an iterator + */ + public Iterator getCandidates() { + return Collections.unmodifiableList(getCandidatesList()).iterator(); + } + + /** + * Get the list of candidates. + * + * @return The candidates list. + */ + public ArrayList getCandidatesList() { + ArrayList res = null; + synchronized (candidates) { + res = new ArrayList(candidates); + } + return res; + } + + /** + * Get the number of transport candidates. + * + * @return The number of transport candidates contained. + */ + public int getCandidatesCount() { + return getCandidatesList().size(); + } + + /** + * Returns the XML element name of the element. + * + * @return the XML element name of the element. + */ + public String getElementName() { + return NODENAME; + } + + /** + * Set the namespace. + * + * @param ns The namespace + */ + protected void setNamespace(final String ns) { + namespace = ns; + } + + /** + * Get the namespace. + * + * @return The namespace + */ + public String getNamespace() { + return namespace; + } + + /** + * Return the XML representation for this element. + */ + public String toXML() { + StringBuilder buf = new StringBuilder(); + + buf.append("<").append(getElementName()).append(" xmlns=\""); + buf.append(getNamespace()).append("\" "); + + synchronized (candidates) { + if (getCandidatesCount() > 0) { + buf.append(">"); + Iterator iter = getCandidates(); + + while (iter.hasNext()) { + JingleTransportCandidate candidate = (JingleTransportCandidate) iter + .next(); + buf.append(candidate.toXML()); + } + buf.append(""); + } else { + buf.append("/>"); + } + } + + return buf.toString(); + } + + /** + * Candidate element in the transport. This class acts as a view of the + * "TransportCandidate" in the Jingle space. + * + * @author Alvaro Saurin + * @see TransportCandidate + */ + public static abstract class JingleTransportCandidate { + + public static final String NODENAME = "candidate"; + + // The transport candidate contained in the element. + protected TransportCandidate transportCandidate; + + /** + * Creates a new TransportNegotiator child. + */ + public JingleTransportCandidate() { + super(); + } + + /** + * Creates a new TransportNegotiator child. + * + * @param candidate the jmf transport candidate + */ + public JingleTransportCandidate(final TransportCandidate candidate) { + super(); + setMediaTransport(candidate); + } + + /** + * Returns the XML element name of the element. + * + * @return the XML element name of the element. + */ + public static String getElementName() { + return NODENAME; + } + + /** + * Get the current transportElement candidate. + * + * @return the transportElement candidate + */ + public TransportCandidate getMediaTransport() { + return transportCandidate; + } + + /** + * Set the transportElement candidate. + * + * @param cand the transportElement candidate + */ + public void setMediaTransport(final TransportCandidate cand) { + if (cand != null) { + transportCandidate = cand; + } + } + + /** + * Get the list of attributes. + * + * @return a string with the list of attributes. + */ + protected String getChildElements() { + return null; + } + + /** + * Obtain a valid XML representation of a trancport candidate + * + * @return A string containing the XML dump of the transport candidate. + */ + public String toXML() { + StringBuilder buf = new StringBuilder(); + String childElements = getChildElements(); + + if (transportCandidate != null && childElements != null) { + buf.append("<").append(getElementName()).append(" "); + buf.append(childElements); + buf.append("/>"); + } + + return buf.toString(); + } + } + + // Subclasses + + /** + * RTP-ICE profile + */ + public static class Ice extends JingleTransport { + public static final String NAMESPACE = "http://jabber.org/protocol/jingle/transport/ice"; + + public Ice() { + super(); + setNamespace(NAMESPACE); + } + + /** + * Add a transport candidate + * + * @see org.jivesoftware.smackx.packet.JingleTransport#addCandidate(org.jivesoftware.smackx.packet.JingleTransport.JingleTransportCandidate) + */ + public void addCandidate(final JingleTransportCandidate candidate) { + super.addCandidate(candidate); + } + + /** + * Get the list of candidates. As a "raw-udp" transport can only contain + * one candidate, we use the first in the list... + * + * @see org.jivesoftware.smackx.packet.JingleTransport#getCandidates() + */ + public ArrayList getCandidatesList() { + ArrayList copy = new ArrayList(); + ArrayList superCandidatesList = super.getCandidatesList(); + for (int i = 0; i < superCandidatesList.size(); i++) { + copy.add(superCandidatesList.get(i)); + } + + return copy; + } + + public static class Candidate extends JingleTransportCandidate { + /** + * Default constructor + */ + public Candidate() { + super(); + } + + /** + * Constructor with a transport candidate. + */ + public Candidate(final TransportCandidate tc) { + super(tc); + } + + /** + * Get the elements of this candidate. + */ + protected String getChildElements() { + StringBuilder buf = new StringBuilder(); + + if (transportCandidate != null) {// && transportCandidate instanceof TransportCandidate.Ice) { + TransportCandidate.Ice tci = (TransportCandidate.Ice) transportCandidate; + + // We convert the transportElement candidate to XML here... + buf.append(" generation=\"").append(tci.getGeneration()).append("\""); + buf.append(" ip=\"").append(tci.getIp()).append("\""); + buf.append(" port=\"").append(tci.getPort()).append("\""); + buf.append(" network=\"").append(tci.getNetwork()).append("\""); + buf.append(" username=\"").append(tci.getUsername()).append("\""); + buf.append(" password=\"").append(tci.getPassword()).append("\""); + buf.append(" preference=\"").append(tci.getPreference()).append("\""); + + // Optional elements + if (transportCandidate.getName() != null) { + buf.append(" name=\"").append(tci.getName()).append("\""); + } + } + + return buf.toString(); + } + + } + } + + /** + * Raw UDP profile. + */ + public static class RawUdp extends JingleTransport { + public static final String NAMESPACE = "http://jabber.org/protocol/jingle/transport/raw-udp"; + + public RawUdp() { + super(); + setNamespace(NAMESPACE); + } + + /** + * Add a transport candidate + * + * @see org.jivesoftware.smackx.packet.JingleTransport#addCandidate(org.jivesoftware.smackx.packet.JingleTransport.JingleTransportCandidate) + */ + public void addCandidate(final JingleTransportCandidate candidate) { + candidates.clear(); + super.addCandidate(candidate); + } + + /** + * Get the list of candidates. As a "raw-udp" transport can only contain + * one candidate, we use the first in the list... + * + * @see org.jivesoftware.smackx.packet.JingleTransport#getCandidates() + */ + public ArrayList getCandidatesList() { + ArrayList copy = new ArrayList(); + ArrayList superCandidatesList = super.getCandidatesList(); + if (superCandidatesList.size() > 0) { + copy.add(superCandidatesList.get(0)); + } + + return copy; + } + + /** + * Raw-udp transport candidate. + */ + public static class Candidate extends JingleTransportCandidate { + /** + * Default constructor + */ + public Candidate() { + super(); + } + + /** + * Constructor with a transport candidate. + */ + public Candidate(final TransportCandidate tc) { + super(tc); + } + + /** + * Get the elements of this candidate. + */ + protected String getChildElements() { + StringBuilder buf = new StringBuilder(); + + if (transportCandidate != null && transportCandidate instanceof TransportCandidate.Fixed) { + TransportCandidate.Fixed tcf = (TransportCandidate.Fixed) transportCandidate; + + buf.append(" generation=\"").append(tcf.getGeneration()).append("\""); + buf.append(" ip=\"").append(tcf.getIp()).append("\""); + buf.append(" port=\"").append(tcf.getPort()).append("\""); + + // Optional parameters + String name = tcf.getName(); + if (name != null) { + buf.append(" name=\"").append(name).append("\""); + } + } + return buf.toString(); + } + + } + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/packet/package.html b/jingle/extension/source/org/jivesoftware/smackx/packet/package.html new file mode 100644 index 000000000..490d1d72d --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/packet/package.html @@ -0,0 +1 @@ +XML packets that are part of the XMPP extension protocols. \ No newline at end of file diff --git a/jingle/extension/source/org/jivesoftware/smackx/provider/JingleContentDescriptionProvider.java b/jingle/extension/source/org/jivesoftware/smackx/provider/JingleContentDescriptionProvider.java new file mode 100644 index 000000000..6a0d4a174 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/provider/JingleContentDescriptionProvider.java @@ -0,0 +1,125 @@ +package org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smackx.jingle.media.PayloadType; +import org.jivesoftware.smackx.packet.JingleContentDescription; +import org.jivesoftware.smackx.packet.JingleContentDescription.JinglePayloadType; +import org.xmlpull.v1.XmlPullParser; + +/** + * Parser for a Jingle description + * + * @author Alvaro Saurin + */ +public abstract class JingleContentDescriptionProvider implements PacketExtensionProvider { + + /** + * Default constructor + */ + public JingleContentDescriptionProvider() { + super(); + } + + /** + * Parse a iq/jingle/description/payload-type element. + * + * @param parser the input to parse + * @return a payload type element + * @throws Exception + */ + protected JinglePayloadType parsePayload(final XmlPullParser parser) + throws Exception { + int ptId = 0; + String ptName; + int ptChannels = 0; + + try { + ptId = Integer.parseInt(parser.getAttributeValue("", "id")); + } catch (Exception e) { + } + + ptName = parser.getAttributeValue("", "name"); + + try { + ptChannels = Integer.parseInt(parser.getAttributeValue("", "channels")); + } catch (Exception e) { + } + + return new JinglePayloadType(new PayloadType(ptId, ptName, ptChannels)); + } + + /** + * Parse a iq/jingle/description element. + * + * @param parser the input to parse + * @return a description element + * @throws Exception + */ + public PacketExtension parseExtension(final XmlPullParser parser) throws Exception { + boolean done = false; + JingleContentDescription desc = getInstance(); + + while (!done) { + int eventType = parser.next(); + String name = parser.getName(); + + if (eventType == XmlPullParser.START_TAG) { + if (name.equals(JingleContentDescription.JinglePayloadType.NODENAME)) { + desc.addJinglePayloadType(parsePayload(parser)); + } else { + throw new Exception("Unknow element \"" + name + "\" in content."); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (name.equals(JingleContentDescription.NODENAME)) { + done = true; + } + } + } + return desc; + } + + /** + * Return a new instance of this class. Subclasses must overwrite this + * method. + */ + protected abstract JingleContentDescription getInstance(); + + /** + * Jingle audio + */ + public static class Audio extends JingleContentDescriptionProvider { + + /** + * Default constructor + */ + public Audio() { + super(); + } + + /** + * Parse an audio payload type. + */ + public JinglePayloadType parsePayload(final XmlPullParser parser) + throws Exception { + JinglePayloadType pte = super.parsePayload(parser); + PayloadType.Audio pt = new PayloadType.Audio(pte.getPayloadType()); + int ptClockRate = 0; + + try { + ptClockRate = Integer.parseInt(parser.getAttributeValue("", "clockrate")); + } catch (Exception e) { + } + pt.setClockRate(ptClockRate); + + return new JinglePayloadType.Audio(pt); + } + + /** + * Get a new instance of this object. + */ + protected JingleContentDescription getInstance() { + return new JingleContentDescription.Audio(); + } + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/provider/JingleContentInfoProvider.java b/jingle/extension/source/org/jivesoftware/smackx/provider/JingleContentInfoProvider.java new file mode 100644 index 000000000..e6e79ffc0 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/provider/JingleContentInfoProvider.java @@ -0,0 +1,106 @@ +package org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smackx.jingle.media.ContentInfo; +import org.jivesoftware.smackx.packet.JingleContentInfo; +import org.xmlpull.v1.XmlPullParser; + +/** + * Jingle Audio jmf-info provider + * + * @author Alvaro Saurin + */ +public class JingleContentInfoProvider implements PacketExtensionProvider { + + /** + * Creates a new provider. ProviderManager requires that every + * PacketExtensionProvider has a public, no-argument constructor + */ + public JingleContentInfoProvider() { + super(); + } + + public PacketExtension parseExtension(final XmlPullParser parser) throws Exception { + // This method must be overwritten by subclasses... + return null; + } + + /** + * JingleContentDescription.Audio info provider + */ + public static class Audio extends JingleContentInfoProvider { + + private PacketExtension audioInfo; + + /** + * Empty constructor. + */ + public Audio() { + this(null); + } + + /** + * Constructor with an audio info. + * + * @param audioInfo the jmf info + */ + public Audio(final PacketExtension audioInfo) { + super(); + this.audioInfo = audioInfo; + } + + /** + * Parse a JingleContentDescription.Audio extension. + */ + public PacketExtension parseExtension(final XmlPullParser parser) + throws Exception { + PacketExtension result = null; + + if (audioInfo != null) { + result = audioInfo; + } else { + String elementName = parser.getName(); + + // Try to get an Audio content info + ContentInfo mi = ContentInfo.Audio.fromString(elementName); + if (mi != null) { + result = new JingleContentInfo.Audio(mi); + } + } + return result; + } + + // Sub-elements + + public static class Busy extends Audio { + public Busy() { + super(new JingleContentInfo.Audio.Busy()); + } + } + + public static class Hold extends Audio { + public Hold() { + super(new JingleContentInfo.Audio.Hold()); + } + } + + public static class Mute extends Audio { + public Mute() { + super(new JingleContentInfo.Audio.Mute()); + } + } + + public static class Queued extends Audio { + public Queued() { + super(new JingleContentInfo.Audio.Queued()); + } + } + + public static class Ringing extends Audio { + public Ringing() { + super(new JingleContentInfo.Audio.Ringing()); + } + } + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/provider/JingleProvider.java b/jingle/extension/source/org/jivesoftware/smackx/provider/JingleProvider.java new file mode 100644 index 000000000..9abdca003 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/provider/JingleProvider.java @@ -0,0 +1,128 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2005 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.packet.Jingle; +import org.jivesoftware.smackx.packet.JingleContentDescription; +import org.jivesoftware.smackx.packet.JingleContentInfo; +import org.jivesoftware.smackx.packet.JingleTransport; +import org.xmlpull.v1.XmlPullParser; + +/** + * The JingleProvider parses Jingle packets. + * + * @author Alvaro Saurin + */ +public class JingleProvider implements IQProvider { + + /** + * Creates a new provider. ProviderManager requires that every + * PacketExtensionProvider has a public, no-argument constructor + */ + public JingleProvider() { + super(); + } + + /** + * Parse a iq/jingle element. + */ + public IQ parseIQ(final XmlPullParser parser) throws Exception { + + Jingle jingle = new Jingle(); + String sid = ""; + Jingle.Action action; + String initiator = ""; + String responder = ""; + boolean done = false; + + // Sub-elements providers + JingleContentDescriptionProvider jdpAudio = new JingleContentDescriptionProvider.Audio(); + JingleTransportProvider jtpRawUdp = new JingleTransportProvider.RawUdp(); + JingleTransportProvider jtpIce = new JingleTransportProvider.Ice(); + JingleContentInfoProvider jmipAudio = new JingleContentInfoProvider.Audio(); + + int eventType; + String elementName; + String namespace; + + // Get some attributes for the element + sid = parser.getAttributeValue("", "sid"); + action = Jingle.Action.getAction(parser.getAttributeValue("", "action")); + initiator = parser.getAttributeValue("", "initiator"); + responder = parser.getAttributeValue("", "responder"); + + jingle.setSid(sid); + jingle.setAction(action); + jingle.setInitiator(initiator); + jingle.setResponder(responder); + + // Start processing sub-elements + while (!done) { + eventType = parser.next(); + elementName = parser.getName(); + namespace = parser.getNamespace(); + + if (eventType == XmlPullParser.START_TAG) { + + // Parse some well know subelements, depending on the namespaces + // and element names... + + if (elementName.equals(JingleContentDescription.NODENAME) + && namespace.equals(JingleContentDescription.Audio.NAMESPACE)) { + jingle.addDescription((JingleContentDescription) jdpAudio + .parseExtension(parser)); + } else if (elementName.equals(JingleTransport.NODENAME)) { + + // Parse the possible transport namespaces + if (namespace.equals(JingleTransport.RawUdp.NAMESPACE)) { + jingle.addTransport((JingleTransport) jtpRawUdp + .parseExtension(parser)); + } else if (namespace.equals(JingleTransport.Ice.NAMESPACE)) { + jingle.addTransport((JingleTransport) jtpIce + .parseExtension(parser)); + } else { + throw new XMPPException("Unknown transport namespace \"" + + namespace + "\" in Jingle packet."); + } + } else if (namespace.equals(JingleContentInfo.Audio.NAMESPACE)) { + jingle.setContentInfo((JingleContentInfo) jmipAudio + .parseExtension(parser)); + } else if (elementName.equals("content")) { + //TODO Separate Contents (XEP-0166) + } else { + throw new XMPPException("Unknown combination of namespace \"" + + namespace + "\" and element name \"" + elementName + + "\" in Jingle packet."); + } + + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(Jingle.getElementName())) { + done = true; + } + } + } + + return jingle; + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/provider/JingleTransportProvider.java b/jingle/extension/source/org/jivesoftware/smackx/provider/JingleTransportProvider.java new file mode 100644 index 000000000..f87d73969 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/provider/JingleTransportProvider.java @@ -0,0 +1,231 @@ +package org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smackx.jingle.nat.TransportCandidate; +import org.jivesoftware.smackx.packet.JingleTransport; +import org.jivesoftware.smackx.packet.JingleTransport.JingleTransportCandidate; +import org.xmlpull.v1.XmlPullParser; + +/** + * Provider for a Jingle transport element + * + * @author Alvaro Saurin + */ +public abstract class JingleTransportProvider implements PacketExtensionProvider { + + /** + * Creates a new provider. ProviderManager requires that every + * PacketExtensionProvider has a public, no-argument constructor + */ + public JingleTransportProvider() { + super(); + } + + /** + * Obtain the corresponding TransportNegotiator instance. + * + * @return a new TransportNegotiator instance + */ + protected JingleTransport getInstance() { + return new JingleTransport(); + } + + /** + * Parse a iq/jingle/transport element. + * + * @param parser the structure to parse + * @return a transport element. + * @throws Exception + */ + public PacketExtension parseExtension(final XmlPullParser parser) throws Exception { + boolean done = false; + JingleTransport trans = getInstance(); + + while (!done) { + int eventType = parser.next(); + String name = parser.getName(); + + if (eventType == XmlPullParser.START_TAG) { + if (name.equals(JingleTransportCandidate.NODENAME)) { + JingleTransportCandidate jtc = parseCandidate(parser); + if (jtc != null) trans.addCandidate(jtc); + } else { + throw new Exception("Unknown tag \"" + name + "\" in transport element."); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (name.equals(JingleTransport.NODENAME)) { + done = true; + } + } + } + + return trans; + } + + protected abstract JingleTransportCandidate parseCandidate(final XmlPullParser parser) + throws Exception; + + /** + * RTP-ICE profile + */ + public static class Ice extends JingleTransportProvider { + + /** + * Defauls constructor. + */ + public Ice() { + super(); + } + + /** + * Obtain the corresponding TransportNegotiator.Ice instance. + * + * @return a new TransportNegotiator.Ice instance + */ + protected JingleTransport getInstance() { + return new JingleTransport.Ice(); + } + + /** + * Parse a iq/jingle/transport/candidate element. + * + * @param parser the structure to parse + * @return a candidate element + * @throws Exception + */ + protected JingleTransportCandidate parseCandidate(XmlPullParser parser) throws Exception { + TransportCandidate.Ice mt = new TransportCandidate.Ice(); + + String channel = parser.getAttributeValue("", "channel"); + String generation = parser.getAttributeValue("", "generation"); + String ip = parser.getAttributeValue("", "ip"); + String name = parser.getAttributeValue("", "name"); + String network = parser.getAttributeValue("", "network"); + String username = parser.getAttributeValue("", "username"); + String password = parser.getAttributeValue("", "password"); + String port = parser.getAttributeValue("", "port"); + String preference = parser.getAttributeValue("", "preference"); + String proto = parser.getAttributeValue("", "proto"); + + if (channel != null) { + mt.setChannel(new TransportCandidate.Channel(channel)); + } + + if (generation != null) { + try { + mt.setGeneration(Integer.parseInt(generation)); + } catch (Exception e) { + } + } + + if (ip != null) { + mt.setIp(ip); + } else { + return null; + } + + if (name != null) { + mt.setName(name); + } + + if (network != null) { + try { + mt.setNetwork(Integer.parseInt(network)); + } catch (Exception e) { + } + } + + if (username != null) { + mt.setUsername(username); + } + + if (password != null) { + mt.setPassword(password); + } + + if (port != null) { + try { + mt.setPort(Integer.parseInt(port)); + } catch (Exception e) { + } + } + + if (preference != null) { + try { + mt.setPreference(Integer.parseInt(preference)); + } catch (Exception e) { + } + } + + if (proto != null) { + mt.setProto(new TransportCandidate.Protocol(proto)); + } + + return new JingleTransport.Ice.Candidate(mt); + } + } + + /** + * Raw UDP profile + */ + public static class RawUdp extends JingleTransportProvider { + + /** + * Defauls constructor. + */ + public RawUdp() { + super(); + } + + /** + * Obtain the corresponding TransportNegotiator.RawUdp instance. + * + * @return a new TransportNegotiator.RawUdp instance + */ + protected JingleTransport getInstance() { + return new JingleTransport.RawUdp(); + } + + /** + * Parse a iq/jingle/transport/candidate element. + * + * @param parser the structure to parse + * @return a candidate element + * @throws Exception + */ + protected JingleTransportCandidate parseCandidate(XmlPullParser parser) throws Exception { + TransportCandidate.Fixed mt = new TransportCandidate.Fixed(); + + String generation = parser.getAttributeValue("", "generation"); + String ip = parser.getAttributeValue("", "ip"); + String name = parser.getAttributeValue("", "name"); + String port = parser.getAttributeValue("", "port"); + + //System.out.println(); + + if (generation != null) { + try { + mt.setGeneration(Integer.parseInt(generation)); + } catch (Exception e) { + } + } + + if (ip != null) { + mt.setIp(ip); + } + + if (name != null) { + mt.setName(name); + } + + if (port != null) { + try { + mt.setPort(Integer.parseInt(port)); + } catch (Exception e) { + } + } + return new JingleTransport.RawUdp.Candidate(mt); + } + } +} diff --git a/jingle/extension/source/org/jivesoftware/smackx/provider/package.html b/jingle/extension/source/org/jivesoftware/smackx/provider/package.html new file mode 100644 index 000000000..962ba6372 --- /dev/null +++ b/jingle/extension/source/org/jivesoftware/smackx/provider/package.html @@ -0,0 +1 @@ +Provides pluggable parsing logic for Smack extensions. \ No newline at end of file diff --git a/jingle/extension/source/overview.html b/jingle/extension/source/overview.html new file mode 100644 index 000000000..8ec5b8ff7 --- /dev/null +++ b/jingle/extension/source/overview.html @@ -0,0 +1,4 @@ +API specification for Smack, an Open Source XMPP client library. +

+ The {@link org.jivesoftware.smack.XMPPConnection} class is the main entry point for the API. + diff --git a/jingle/extension/test/config/test-case.xml b/jingle/extension/test/config/test-case.xml new file mode 100644 index 000000000..d477c269e --- /dev/null +++ b/jingle/extension/test/config/test-case.xml @@ -0,0 +1,13 @@ + + + + + + localhost + 5222 + + + chat + conference + + \ No newline at end of file diff --git a/jingle/extension/test/org/jivesoftware/smack/test/SmackTestCase.java b/jingle/extension/test/org/jivesoftware/smack/test/SmackTestCase.java new file mode 100644 index 000000000..2784cf9e5 --- /dev/null +++ b/jingle/extension/test/org/jivesoftware/smack/test/SmackTestCase.java @@ -0,0 +1,368 @@ +/** + * $RCSfile$ + * $Revision: 5367 $ + * $Date: 2006-09-14 16:16:40 -0300 (qui, 14 set 2006) $ + * + * Copyright 2003-2005 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.test; + +import junit.framework.TestCase; +import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.xmlpull.mxp1.MXParser; +import org.xmlpull.v1.XmlPullParser; + +import javax.net.SocketFactory; +import java.io.InputStream; +import java.net.URL; +import java.util.Enumeration; + +/** + * Base class for all the test cases which provides a pre-configured execution context. This + * means that any test case that subclassifies this base class will have access to a pool of + * connections and to the user of each connection. The maximum number of connections in the pool + * can be controlled by the message {@link #getMaxConnections()} which every subclass must + * implement.

+ * + * This base class defines a default execution context (i.e. host, port, chat domain and muc + * domain) which can be found in the file "config/test-case.xml". However, each subclass could + * redefine the default configuration by providing its own configuration file (if desired). The + * name of the configuration file must be of the form .xml (e.g. RosterTest.xml). + * The file must be placed in the folder "config". This folder is where the default configuration + * file is being held. + * + * @author Gaston Dombiak + */ +public abstract class SmackTestCase extends TestCase { + + private String host = "localhost"; + private String serviceName = "localhost"; + private int port = 5222; + + private String chatDomain = "chat"; + private String mucDomain = "conference"; + + private XMPPConnection[] connections = null; + + /** + * Constructor for SmackTestCase. + * @param arg0 + */ + public SmackTestCase(String arg0) { + super(arg0); + } + + /** + * Returns the maximum number of connections to initialize for this test case. All the + * initialized connections will be connected to the server using a new test account for + * each conection. + * + * @return the maximum number of connections to initialize for this test case. + */ + protected abstract int getMaxConnections(); + + /** + * Returns a SocketFactory that will be used to create the socket to the XMPP server. By + * default no SocketFactory is used but subclasses my want to redefine this method.

+ * + * A custom SocketFactory allows fine-grained control of the actual connection to the XMPP + * server. A typical use for a custom SocketFactory is when connecting through a SOCKS proxy. + * + * @return a SocketFactory that will be used to create the socket to the XMPP server. + */ + protected SocketFactory getSocketFactory() { + return null; + } + + /** + * Returns the XMPPConnection located at the requested position. Each test case holds a + * pool of connections which is initialized while setting up the test case. The maximum + * number of connections is controlled by the message {@link #getMaxConnections()} which + * every subclass must implement.

+ * + * If the requested position is greater than the connections size then an + * IllegalArgumentException will be thrown. + * + * @param index the position in the pool of the connection to look for. + * @return the XMPPConnection located at the requested position. + */ + protected XMPPConnection getConnection(int index) { + if (index > getMaxConnections()) { + throw new IllegalArgumentException("Index out of bounds"); + } + return connections[index]; + } + + /** + * Returns the name of the user (e.g. johndoe) that is using the connection + * located at the requested position. + * + * @param index the position in the pool of the connection to look for. + * @return the user of the user (e.g. johndoe). + */ + protected String getUsername(int index) { + if (index > getMaxConnections()) { + throw new IllegalArgumentException("Index out of bounds"); + } + return "user" + index; + } + + /** + * Returns the bare XMPP address of the user (e.g. johndoe@jabber.org) that is + * using the connection located at the requested position. + * + * @param index the position in the pool of the connection to look for. + * @return the bare XMPP address of the user (e.g. johndoe@jabber.org). + */ + protected String getBareJID(int index) { + return getUsername(index) + "@" + getConnection(index).getServiceName(); + } + + /** + * Returns the full XMPP address of the user (e.g. johndoe@jabber.org/Smack) that is + * using the connection located at the requested position. + * + * @param index the position in the pool of the connection to look for. + * @return the full XMPP address of the user (e.g. johndoe@jabber.org/Smack). + */ + protected String getFullJID(int index) { + return getBareJID(index) + "/Smack"; + } + + protected String getHost() { + return host; + } + + protected int getPort() { + return port; + } + + protected String getServiceName() { + return serviceName; + } + + /** + * Returns the default groupchat service domain. + * + * @return the default groupchat service domain. + */ + protected String getChatDomain() { + return chatDomain; + } + + /** + * Returns the default MUC service domain. + * + * @return the default MUC service domain. + */ + protected String getMUCDomain() { + return mucDomain + "." + serviceName; + } + + protected void setUp() throws Exception { + super.setUp(); + init(); + if (getMaxConnections() < 1) { + return; + } + connections = new XMPPConnection[getMaxConnections()]; + try { + // Connect to the server + for (int i = 0; i < getMaxConnections(); i++) { + // Create the configuration for this new connection + ConnectionConfiguration config = new ConnectionConfiguration(host, port); + config.setTLSEnabled(true); + config.setCompressionEnabled(Boolean.getBoolean("test.compressionEnabled")); + config.setSASLAuthenticationEnabled(true); + if (getSocketFactory() == null) { + connections[i] = new XMPPConnection(config); + } + else { + connections[i] = new XMPPConnection(config, getSocketFactory()); + } + connections[i].connect(); + } + // Use the host name that the server reports. This is a good idea in most + // cases, but could fail if the user set a hostname in their XMPP server + // that will not resolve as a network connection. + host = connections[0].getHost(); + serviceName = connections[0].getServiceName(); + // Create the test accounts + if (!getConnection(0).getAccountManager().supportsAccountCreation()) + fail("Server does not support account creation"); + + for (int i = 0; i < getMaxConnections(); i++) { + // Create the test account + try { + getConnection(i).getAccountManager().createAccount("user" + i, "user" + i); + } catch (XMPPException e) { + // Do nothing if the accout already exists + if (e.getXMPPError() == null || e.getXMPPError().getCode() != 409) { + throw e; + } + } + // Login with the new test account + getConnection(i).login("user" + i, "user" + i); + } + // Let the server process the available presences + Thread.sleep(150); + } + catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } + + protected void tearDown() throws Exception { + super.tearDown(); + + for (int i = 0; i < getMaxConnections(); i++) { + if (getConnection(i).isConnected()) { + // Delete the created account for the test + getConnection(i).getAccountManager().deleteAccount(); + // Close the connection + getConnection(i).disconnect(); + } + } + } + + /** + * Initializes the context of the test case. We will first try to load the configuration from + * a file whose name is conformed by the test case class name plus an .xml extension + * (e.g RosterTest.xml). If no file was found under that name then we will try to load the + * default configuration for all the test cases from the file "config/test-case.xml". + * + */ + private void init() { + try { + boolean found = false; + // Try to load the configutation from an XML file specific for this test case + Enumeration resources = + ClassLoader.getSystemClassLoader().getResources(getConfigurationFilename()); + while (resources.hasMoreElements()) { + found = parseURL((URL) resources.nextElement()); + } + // If none was found then try to load the configuration from the default configuration + // file (i.e. "config/test-case.xml") + if (!found) { + resources = ClassLoader.getSystemClassLoader().getResources("config/test-case.xml"); + while (resources.hasMoreElements()) { + found = parseURL((URL) resources.nextElement()); + } + } + if (!found) { + System.err.println("File config/test-case.xml not found. Using default config."); + } + } + catch (Exception e) { + } + } + + /** + * Returns true if the given URL was found and parsed without problems. The file provided + * by the URL must contain information useful for the test case configuration, such us, + * host and port of the server. + * + * @param url the url of the file to parse. + * @return true if the given URL was found and parsed without problems. + */ + private boolean parseURL(URL url) { + boolean parsedOK = false; + InputStream systemStream = null; + try { + systemStream = url.openStream(); + XmlPullParser parser = new MXParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(systemStream, "UTF-8"); + int eventType = parser.getEventType(); + do { + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("host")) { + host = parser.nextText(); + } + else if (parser.getName().equals("port")) { + port = parseIntProperty(parser, port); + } + else if (parser.getName().equals("serviceName")) { + serviceName = parser.nextText(); + } + else if (parser.getName().equals("chat")) { + chatDomain = parser.nextText(); + } + else if (parser.getName().equals("muc")) { + mucDomain = parser.nextText(); + } + } + eventType = parser.next(); + } + while (eventType != XmlPullParser.END_DOCUMENT); + parsedOK = true; + } + catch (Exception e) { + e.printStackTrace(); + } + finally { + try { + systemStream.close(); + } + catch (Exception e) { + } + } + return parsedOK; + } + + private static int parseIntProperty(XmlPullParser parser, int defaultValue) throws Exception { + try { + return Integer.parseInt(parser.nextText()); + } + catch (NumberFormatException nfe) { + nfe.printStackTrace(); + return defaultValue; + } + } + + /** + * Returns the name of the configuration file related to this test case. By default all + * the test cases will use the same configuration file. However, it's possible to override the + * default configuration by providing a file of the form .xml + * (e.g. RosterTest.xml). + * + * @return the name of the configuration file related to this test case. + */ + private String getConfigurationFilename() { + String fullClassName = this.getClass().getName(); + int firstChar = fullClassName.lastIndexOf('.') + 1; + return "config/" + fullClassName.substring(firstChar) + ".xml"; + } + + /** + * Compares two contents of two byte arrays to make sure that they are equal + * + * @param message The message to show in the case of failure + * @param byteArray1 The first byte array. + * @param byteArray2 The second byte array. + */ + public static void assertEquals(String message, byte [] byteArray1, byte [] byteArray2) { + if(byteArray1.length != byteArray2.length) { + fail(message); + } + for(int i = 0; i < byteArray1.length; i++) { + assertEquals(message, byteArray1[i], byteArray2[i]); + } + } + +} diff --git a/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleManagerTest.java b/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleManagerTest.java new file mode 100644 index 000000000..5d252ff9f --- /dev/null +++ b/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleManagerTest.java @@ -0,0 +1,1016 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright (C) 2002-2006 Jive Software. All rights reserved. + * ==================================================================== + * The Jive Software License (based on Apache Software License, Version 1.1) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by + * Jive Software (http://www.jivesoftware.com)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Smack" and "Jive Software" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please + * contact webmaster@jivesoftware.com. + * + * 5. Products derived from this software may not be called "Smack", + * nor may "Smack" appear in their name, without prior written + * permission of Jive Software. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL JIVE SOFTWARE OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + */ + +package org.jivesoftware.smackx.jingle; + +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.provider.ProviderManager; +import org.jivesoftware.smack.test.SmackTestCase; +import org.jivesoftware.smackx.jingle.listeners.JingleSessionListener; +import org.jivesoftware.smackx.jingle.listeners.JingleSessionRequestListener; +import org.jivesoftware.smackx.jingle.media.JingleMediaManager; +import org.jivesoftware.smackx.jingle.media.JingleMediaSession; +import org.jivesoftware.smackx.jingle.media.PayloadType; +import org.jivesoftware.smackx.jingle.nat.*; +import org.jivesoftware.smackx.packet.Jingle; +import org.jivesoftware.smackx.provider.JingleProvider; + +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.util.ArrayList; + +/** + * Test the Jingle extension using the high level API + *

+ * + * @author Alvaro Saurin + */ +public class JingleManagerTest extends SmackTestCase { + + private int counter; + + private final Object mutex = new Object(); + + /** + * Constructor for JingleManagerTest. + * + * @param name + */ + public JingleManagerTest(final String name) { + super(name); + + resetCounter(); + } + + // Counter management + + private void resetCounter() { + synchronized (mutex) { + counter = 0; + } + } + + public void incCounter() { + synchronized (mutex) { + counter++; + } + } + + private int valCounter() { + int val; + synchronized (mutex) { + val = counter; + } + return val; + } + + /** + * Generate a list of payload types + * + * @return A testing list + */ + private ArrayList getTestPayloads1() { + ArrayList result = new ArrayList(); + + result.add(new PayloadType.Audio(34, "supercodec-1", 2, 14000)); + result.add(new PayloadType.Audio(56, "supercodec-2", 1, 44000)); + result.add(new PayloadType.Audio(36, "supercodec-3", 2, 28000)); + result.add(new PayloadType.Audio(45, "supercodec-4", 1, 98000)); + + return result; + } + + private ArrayList getTestPayloads2() { + ArrayList result = new ArrayList(); + + result.add(new PayloadType.Audio(27, "supercodec-3", 2, 28000)); + result.add(new PayloadType.Audio(56, "supercodec-2", 1, 44000)); + result.add(new PayloadType.Audio(32, "supercodec-4", 1, 98000)); + result.add(new PayloadType.Audio(34, "supercodec-1", 2, 14000)); + + return result; + } + + private ArrayList getTestPayloads3() { + ArrayList result = new ArrayList(); + + result.add(new PayloadType.Audio(91, "badcodec-1", 2, 28000)); + result.add(new PayloadType.Audio(92, "badcodec-2", 1, 44000)); + result.add(new PayloadType.Audio(93, "badcodec-3", 1, 98000)); + result.add(new PayloadType.Audio(94, "badcodec-4", 2, 14000)); + + return result; + } + + /** + * Test for the session request detection. Here, we use the same filter we + * use in the JingleManager... + */ + public void testInitJingleSessionRequestListeners() { + + resetCounter(); + + ProviderManager.getInstance().addIQProvider("jingle", + "http://jabber.org/protocol/jingle", + new JingleProvider()); + + PacketFilter initRequestFilter = new PacketFilter() { + // Return true if we accept this packet + public boolean accept(Packet pin) { + if (pin instanceof IQ) { + System.out.println("packet: " + pin.toXML()); + IQ iq = (IQ) pin; + if (iq.getType().equals(IQ.Type.SET)) { + System.out.println("packet"); + if (iq instanceof Jingle) { + Jingle jin = (Jingle) pin; + if (jin.getAction().equals(Jingle.Action.SESSIONINITIATE)) { + System.out + .println("Session initiation packet accepted... "); + return true; + } + } + } + } + return false; + } + }; + + // Start a packet listener for session initiation requests + getConnection(0).addPacketListener(new PacketListener() { + public void processPacket(final Packet packet) { + System.out.println("Packet detected... "); + incCounter(); + } + }, initRequestFilter); + + // Create a dummy packet for testing... + IQfake iqSent = new IQfake( + " " + + ""); + + iqSent.setTo(getFullJID(0)); + iqSent.setFrom(getFullJID(0)); + iqSent.setType(IQ.Type.SET); + + System.out.println("Sending packet and waiting... "); + getConnection(1).sendPacket(iqSent); + try { + Thread.sleep(10000); + } + catch (InterruptedException e) { + } + + System.out.println("Awake... " + valCounter()); + assertTrue(valCounter() > 0); + } + + /** + * High level API test. This is a simple test to use with a XMPP client and + * check if the client receives the message 1. User_1 will send an + * invitation to user_2. + */ + public void testSendSimpleMessage() { + + resetCounter(); + + try { + TransportResolver tr1 = new FixedResolver("127.0.0.1", 54222); + TransportResolver tr2 = new FixedResolver("127.0.0.1", 54567); + + JingleManager man0 = new JingleManager(getConnection(0), tr1); + JingleManager man1 = new JingleManager(getConnection(1), tr2); + + // Session 1 waits for connections + man1.addJingleSessionRequestListener(new JingleSessionRequestListener() { + /** + * Called when a new session request is detected + */ + public void sessionRequested(final JingleSessionRequest request) { + incCounter(); + System.out.println("Session request detected, from " + + request.getFrom()); + } + }); + + // Session 0 starts a request + System.out.println("Starting session request, to " + getFullJID(1) + "..."); + OutgoingJingleSession session0 = man0.createOutgoingJingleSession( + getFullJID(1), getTestPayloads1()); + session0.start(null); + + Thread.sleep(5000); + + assertTrue(valCounter() > 0); + + } + catch (Exception e) { + e.printStackTrace(); + fail("An error occured with Jingle"); + } + } + + /** + * High level API test. This is a simple test to use with a XMPP client and + * check if the client receives the message 1. User_1 will send an + * invitation to user_2. + */ + public void testAcceptJingleSession() { + + resetCounter(); + + try { + TransportResolver tr1 = new FixedResolver("127.0.0.1", 54222); + TransportResolver tr2 = new FixedResolver("127.0.0.1", 54567); + + final JingleManager man0 = new JingleManager(getConnection(0), tr1); + final JingleManager man1 = new JingleManager(getConnection(1), tr2); + + man1.addJingleSessionRequestListener(new JingleSessionRequestListener() { + /** + * Called when a new session request is detected + */ + public void sessionRequested(final JingleSessionRequest request) { + incCounter(); + System.out.println("Session request detected, from " + + request.getFrom() + ": accepting."); + + // We accept the request + try { + IncomingJingleSession session1 = request.accept(getTestPayloads2()); + session1.start(request); + } + catch (Exception e) { + e.printStackTrace(); + } + } + }); + + // Session 0 starts a request + System.out.println("Starting session request, to " + getFullJID(1) + "..."); + OutgoingJingleSession session0 = man0.createOutgoingJingleSession( + getFullJID(1), getTestPayloads1()); + session0.start(null); + + Thread.sleep(20000); + + assertTrue(valCounter() > 0); + + } + catch (Exception e) { + e.printStackTrace(); + fail("An error occured with Jingle"); + } + } + + /** + * This is a simple test where both endpoints have exactly the same payloads + * and the session is accepted. + */ + public void testEqualPayloadsSetSession() { + + resetCounter(); + + try { + TransportResolver tr1 = new FixedResolver("127.0.0.1", 54213); + TransportResolver tr2 = new FixedResolver("127.0.0.1", 54531); + + final JingleManager man0 = new JingleManager(getConnection(0), tr1); + final JingleManager man1 = new JingleManager(getConnection(1), tr2); + + man1.addJingleSessionRequestListener(new JingleSessionRequestListener() { + /** + * Called when a new session request is detected + */ + public void sessionRequested(final JingleSessionRequest request) { + System.out.println("Session request detected, from " + + request.getFrom() + ": accepting."); + try { + // We accept the request + IncomingJingleSession session1 = request.accept(getTestPayloads1()); + + session1.addListener(new JingleSessionListener() { + public void sessionClosed(String reason, JingleSession jingleSession) { + System.out.println("sessionClosed()."); + } + + public void sessionClosedOnError(XMPPException e, JingleSession jingleSession) { + System.out.println("sessionClosedOnError()."); + } + + public void sessionDeclined(String reason, JingleSession jingleSession) { + System.out.println("sessionDeclined()."); + } + + public void sessionEstablished(PayloadType pt, + TransportCandidate rc, TransportCandidate lc, JingleSession jingleSession) { + incCounter(); + System.out + .println("Responder: the session is fully established."); + System.out.println("+ Payload Type: " + pt.getId()); + System.out.println("+ Local IP/port: " + lc.getIp() + ":" + + lc.getPort()); + System.out.println("+ Remote IP/port: " + rc.getIp() + ":" + + rc.getPort()); + } + + public void sessionRedirected(String redirection, JingleSession jingleSession) { + } + }); + + session1.start(request); + } + catch (Exception e) { + e.printStackTrace(); + } + } + }); + + // Session 0 starts a request + System.out.println("Starting session request with equal payloads, to " + + getFullJID(1) + "..."); + OutgoingJingleSession session0 = man0.createOutgoingJingleSession( + getFullJID(1), getTestPayloads1()); + session0.start(null); + + Thread.sleep(20000); + + assertTrue(valCounter() == 1); + + } + catch (Exception e) { + e.printStackTrace(); + fail("An error occured with Jingle"); + } + } + + /** + * This is a simple test where the user_2 rejects the Jingle session. + */ + public void testStagesSession() { + + resetCounter(); + + try { + TransportResolver tr1 = new FixedResolver("127.0.0.1", 54222); + TransportResolver tr2 = new FixedResolver("127.0.0.1", 54567); + + final JingleManager man0 = new JingleManager(getConnection(0), tr1); + final JingleManager man1 = new JingleManager(getConnection(1), tr2); + + man1.addJingleSessionRequestListener(new JingleSessionRequestListener() { + /** + * Called when a new session request is detected + */ + public void sessionRequested(final JingleSessionRequest request) { + System.out.println("Session request detected, from " + + request.getFrom() + ": accepting."); + try { + // We accept the request + IncomingJingleSession session1 = request.accept(getTestPayloads2()); + + session1.addListener(new JingleSessionListener() { + public void sessionClosed(String reason, JingleSession jingleSession) { + System.out.println("sessionClosed()."); + } + + public void sessionClosedOnError(XMPPException e, JingleSession jingleSession) { + System.out.println("sessionClosedOnError()."); + } + + public void sessionDeclined(String reason, JingleSession jingleSession) { + System.out.println("sessionDeclined()."); + } + + public void sessionEstablished(PayloadType pt, + TransportCandidate rc, final TransportCandidate lc, JingleSession jingleSession) { + incCounter(); + System.out + .println("Responder: the session is fully established."); + System.out.println("+ Payload Type: " + pt.getId()); + System.out.println("+ Local IP/port: " + lc.getIp() + ":" + + lc.getPort()); + System.out.println("+ Remote IP/port: " + rc.getIp() + ":" + + rc.getPort()); + } + + public void sessionRedirected(String redirection, JingleSession jingleSession) { + } + }); + + + session1.start(request); + } + catch (Exception e) { + e.printStackTrace(); + } + } + }); + + // Session 0 starts a request + System.out.println("Starting session request, to " + getFullJID(1) + "..."); + OutgoingJingleSession session0 = man0.createOutgoingJingleSession( + getFullJID(1), getTestPayloads1()); + + session0.addListener(new JingleSessionListener() { + public void sessionClosed(String reason, JingleSession jingleSession) { + } + + public void sessionClosedOnError(XMPPException e, JingleSession jingleSession) { + } + + public void sessionDeclined(String reason, JingleSession jingleSession) { + } + + public void sessionEstablished(PayloadType pt, + TransportCandidate rc, TransportCandidate lc, JingleSession jingleSession) { + incCounter(); + System.out.println("Initiator: the session is fully established."); + System.out.println("+ Payload Type: " + pt.getId()); + System.out.println("+ Local IP/port: " + lc.getIp() + ":" + + lc.getPort()); + System.out.println("+ Remote IP/port: " + rc.getIp() + ":" + + rc.getPort()); + } + + public void sessionRedirected(String redirection, JingleSession jingleSession) { + } + }); + session0.start(null); + + Thread.sleep(20000); + + assertTrue(valCounter() == 2); + + } + catch (Exception e) { + e.printStackTrace(); + fail("An error occured with Jingle"); + } + } + + /** + * This is a simple test where the user_2 rejects the Jingle session. + */ + public void testRejectSession() { + + resetCounter(); + + try { + TransportResolver tr1 = new FixedResolver("127.0.0.1", 54222); + TransportResolver tr2 = new FixedResolver("127.0.0.1", 54567); + + final JingleManager man0 = new JingleManager(getConnection(0), tr1); + final JingleManager man1 = new JingleManager(getConnection(1), tr2); + + man1.addJingleSessionRequestListener(new JingleSessionRequestListener() { + /** + * Called when a new session request is detected + */ + public void sessionRequested(final JingleSessionRequest request) { + System.out.println("Session request detected, from " + + request.getFrom() + ": rejecting."); + + // We reject the request + request.reject(); + } + }); + + // Session 0 starts a request + System.out.println("Starting session request, to " + getFullJID(1) + "..."); + OutgoingJingleSession session0 = man0.createOutgoingJingleSession( + getFullJID(1), getTestPayloads1()); + + session0.addListener(new JingleSessionListener() { + public void sessionClosed(String reason, JingleSession jingleSession) { + } + + public void sessionClosedOnError(XMPPException e, JingleSession jingleSession) { + } + + public void sessionDeclined(String reason, JingleSession jingleSession) { + incCounter(); + System.out + .println("The session has been detected as rejected with reason: " + + reason); + } + + public void sessionEstablished(PayloadType pt, + TransportCandidate rc, TransportCandidate lc, JingleSession jingleSession) { + } + + public void sessionRedirected(String redirection, JingleSession jingleSession) { + } + }); + + session0.start(null); + + Thread.sleep(20000); + + assertTrue(valCounter() > 0); + + } + catch (Exception e) { + e.printStackTrace(); + fail("An error occured with Jingle"); + } + } + + /** + * RTP Bridge Test + */ + public void testRTPBridge() { + + resetCounter(); + + try { + + ProviderManager.getInstance().addIQProvider(RTPBridge.NAME, + RTPBridge.NAMESPACE, new RTPBridge.Provider()); + + XMPPConnection x2 = new XMPPConnection("thiago"); + x2.connect(); + x2.login("barata6", "barata6"); + + RTPBridge response = RTPBridge.getRTPBridge(x2, "102"); + + class Listener implements Runnable { + private byte[] buf = new byte[5000]; + private DatagramSocket dataSocket; + private DatagramPacket packet; + + public Listener(DatagramSocket dataSocket) { + this.dataSocket = dataSocket; + } + + public void run() { + try { + while (true) { + // Block until a datagram appears: + packet = new DatagramPacket(buf, buf.length); + dataSocket.receive(packet); + incCounter(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + try { + byte packet[] = {0, 0, 1, 1, 1, 1, 1}; + DatagramSocket ds0 = new DatagramSocket(14004, InetAddress.getByName("0.0.0.0")); + DatagramSocket ds1 = new DatagramSocket(14050, InetAddress.getByName("0.0.0.0")); + DatagramPacket echo0 = new DatagramPacket(packet, packet.length, + InetAddress.getLocalHost(), response.getPortA()); + DatagramPacket echo1 = new DatagramPacket(packet, packet.length, + InetAddress.getLocalHost(), response.getPortB()); + + ds1.send(echo1); + ds0.send(echo0); + + Thread.sleep(500); + + Thread t0 = new Thread(new Listener(ds0)); + Thread t1 = new Thread(new Listener(ds1)); + + t0.start(); + t1.start(); + + int repeat = 300; + + for (int i = 0; i < repeat; i++) { + ds0.send(echo0); + ds1.send(echo1); + Thread.sleep(200); + } + + System.out.println(valCounter()); + assertTrue(valCounter() == repeat * 2 + 1); + + t0.stop(); + t1.stop(); + + ds0.close(); + ds1.close(); + + } catch (Exception e) { + e.printStackTrace(); + } finally { + } + + } catch (Exception e) { + e.printStackTrace(); + } finally { + } + + } + + /** + * This is a full test in the Jingle API. + */ + public void testFullTest() { + + resetCounter(); + + try { + + XMPPConnection.DEBUG_ENABLED = true; + + XMPPConnection x0 = new XMPPConnection("thiago"); + XMPPConnection x1 = new XMPPConnection("thiago"); + + x0.connect(); + x0.login("barata7", "barata7"); + x1.connect(); + x1.login("barata6", "barata6"); + + final JingleManager jm0 = new JingleManager( + x0, new STUNResolver() { + }); + final JingleManager jm1 = new JingleManager( + x1, new FixedResolver("127.0.0.1", 20040)); + +// JingleManager jm0 = new JingleSessionManager( +// x0, new ICEResolver()); +// JingleManager jm1 = new JingleSessionManager( +// x1, new ICEResolver()); + + JingleMediaManager jingleMediaManager = new JingleMediaManager() { + // Media Session Implementation + public JingleMediaSession createMediaSession(final PayloadType payloadType, final TransportCandidate remote, final TransportCandidate local) { + return new JingleMediaSession(payloadType, remote, local) { + + public void initialize() { + } + + public void startTrasmit() { + } + + public void startReceive() { + } + + public void setTrasmit(boolean active) { + } + + public void stopTrasmit() { + } + + public void stopReceive() { + } + }; + } + }; + + jingleMediaManager.addPayloadType(new PayloadType.Audio(3, "GSM", 1, 16000)); + + jm0.setMediaManager(jingleMediaManager); + jm1.setMediaManager(jingleMediaManager); + + jm1.addJingleSessionRequestListener(new JingleSessionRequestListener() { + public void sessionRequested(final JingleSessionRequest request) { + + try { + + IncomingJingleSession session = request.accept(jm1.getMediaManager().getPayloads()); + session.addListener(new JingleSessionListener() { + + public void sessionEstablished(PayloadType pt, TransportCandidate rc, TransportCandidate lc, JingleSession jingleSession) { + //To change body of implemented methods use File | Settings | File Templates. + incCounter(); + System.out.println("Establish In"); + } + + public void sessionDeclined(String reason, JingleSession jingleSession) { + //To change body of implemented methods use File | Settings | File Templates. + } + + public void sessionRedirected(String redirection, JingleSession jingleSession) { + //To change body of implemented methods use File | Settings | File Templates. + } + + public void sessionClosed(String reason, JingleSession jingleSession) { + //To change body of implemented methods use File | Settings | File Templates. + } + + public void sessionClosedOnError(XMPPException e, JingleSession jingleSession) { + //To change body of implemented methods use File | Settings | File Templates. + } + }); + + session.start(); + } catch (XMPPException e) { + e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. + } + + } + }); + + OutgoingJingleSession js0 = jm0.createOutgoingJingleSession("barata6@thiago/Smack"); + + js0.addListener(new JingleSessionListener() { + + public void sessionEstablished(PayloadType pt, TransportCandidate rc, TransportCandidate lc, JingleSession jingleSession) { + //To change body of implemented methods use File | Settings | File Templates. + incCounter(); + System.out.println("Establish Out"); + } + + public void sessionDeclined(String reason, JingleSession jingleSession) { + //To change body of implemented methods use File | Settings | File Templates. + } + + public void sessionRedirected(String redirection, JingleSession jingleSession) { + //To change body of implemented methods use File | Settings | File Templates. + } + + public void sessionClosed(String reason, JingleSession jingleSession) { + //To change body of implemented methods use File | Settings | File Templates. + } + + public void sessionClosedOnError(XMPPException e, JingleSession jingleSession) { + //To change body of implemented methods use File | Settings | File Templates. + } + }); + + js0.start(); + + Thread.sleep(12000); + js0.terminate(); + + assertTrue(valCounter() == 2); + //Thread.sleep(15000); + + } catch (Exception e) { + e.printStackTrace(); + } + + } + + /** + * This is a full test in the Jingle API. + */ + public void testMediaManager() { + + resetCounter(); + + try { + + //XMPPConnection.DEBUG_ENABLED = true; + + XMPPConnection x0 = new XMPPConnection("thiago"); + XMPPConnection x1 = new XMPPConnection("thiago"); + + x0.connect(); + x0.login("barata7", "barata7"); + x1.connect(); + x1.login("barata6", "barata6"); + + final JingleManager jm0 = new JingleManager( + x0, new FixedResolver("127.0.0.1", 20004)); + final JingleManager jm1 = new JingleManager( + x1, new FixedResolver("127.0.0.1", 20040)); + +// JingleManager jm0 = new JingleSessionManager( +// x0, new ICEResolver()); +// JingleManager jm1 = new JingleSessionManager( +// x1, new ICEResolver()); + + JingleMediaManager jingleMediaManager = new JingleMediaManager() { + // Media Session Implementation + public JingleMediaSession createMediaSession(final PayloadType payloadType, final TransportCandidate remote, final TransportCandidate local) { + return new JingleMediaSession(payloadType, remote, local) { + + public void initialize() { + + } + + public void startTrasmit() { + incCounter(); + System.out.println("Transmit"); + } + + public void startReceive() { + incCounter(); + System.out.println("Receive"); + } + + public void setTrasmit(boolean active) { + } + + public void stopTrasmit() { + incCounter(); + System.out.println("Stop Transmit"); + } + + public void stopReceive() { + incCounter(); + System.out.println("Stop Receive"); + } + }; + } + }; + + jingleMediaManager.addPayloadType(new PayloadType.Audio(3, "GSM", 1, 16000)); + + jm0.setMediaManager(jingleMediaManager); + jm1.setMediaManager(jingleMediaManager); + + jm1.addJingleSessionRequestListener(new JingleSessionRequestListener() { + public void sessionRequested(final JingleSessionRequest request) { + + try { + + IncomingJingleSession session = request.accept(jm1.getMediaManager().getPayloads()); + + session.start(request); + } catch (XMPPException e) { + e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. + } + + } + }); + + OutgoingJingleSession js0 = jm0.createOutgoingJingleSession("barata6@thiago/Smack"); + + js0.start(); + + Thread.sleep(10000); + js0.terminate(); + + Thread.sleep(3000); + + System.out.println(valCounter()); + + assertTrue(valCounter() == 8); + //Thread.sleep(15000); + + } catch (Exception e) { + e.printStackTrace(); + } + + } + + /** + * This is a simple test where the user_2 rejects the Jingle session. + */ + public void testIncompatibleCodecs() { + + resetCounter(); + + try { + TransportResolver tr1 = new FixedResolver("127.0.0.1", 54222); + TransportResolver tr2 = new FixedResolver("127.0.0.1", 54567); + + final JingleManager man0 = new JingleManager(getConnection(0), tr1); + final JingleManager man1 = new JingleManager(getConnection(1), tr2); + + man1.addJingleSessionRequestListener(new JingleSessionRequestListener() { + /** + * Called when a new session request is detected + */ + public void sessionRequested + ( + final JingleSessionRequest request) { + System.out.println("Session request detected, from " + + request.getFrom() + ": accepting."); + + try { + // We reject the request + IncomingJingleSession ses = request.accept(getTestPayloads3()); + + ses.start(request); + } + catch (Exception e) { + e.printStackTrace(); + } + } + }); + + // Session 0 starts a request + System.out.println("Starting session request, to " + getFullJID(1) + "..."); + OutgoingJingleSession session0 = man0.createOutgoingJingleSession( + getFullJID(1), getTestPayloads1()); + + session0.addListener(new JingleSessionListener() { + public void sessionClosed(String reason, JingleSession jingleSession) { + } + + public void sessionClosedOnError(XMPPException e, JingleSession jingleSession) { + incCounter(); + System.out + .println("The session has been close on error with reason: " + + e.getMessage()); + } + + public void sessionDeclined(String reason, JingleSession jingleSession) { + incCounter(); + System.out + .println("The session has been detected as rejected with reason: " + + reason); + } + + public void sessionEstablished(PayloadType pt, + TransportCandidate rc, TransportCandidate lc, JingleSession jingleSession) { + } + + public void sessionRedirected(String redirection, JingleSession jingleSession) { + } + }); + + session0.start(null); + + Thread.sleep(20000); + + assertTrue(valCounter() > 0); + + } + catch (Exception e) { + e.printStackTrace(); + fail("An error occured with Jingle"); + } + } + + protected int getMaxConnections() { + return 2; + } + + /** + * Simple class for testing an IQ... + * + * @author Alvaro Saurin + */ + private class IQfake extends IQ { + + private String s; + + public IQfake(final String s) { + super(); + this.s = s; + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append(s); + return buf.toString(); + } + } +} diff --git a/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleSessionTest.java b/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleSessionTest.java new file mode 100644 index 000000000..5292844a1 --- /dev/null +++ b/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleSessionTest.java @@ -0,0 +1,51 @@ +package org.jivesoftware.smackx.jingle; + +import org.jivesoftware.smack.test.SmackTestCase; +import org.jivesoftware.smackx.jingle.nat.BasicResolver; + +public class JingleSessionTest extends SmackTestCase { + + public JingleSessionTest(final String name) { + super(name); + } + + public void testEqualsObject() { + JingleSession js1 = new OutgoingJingleSession(getConnection(0), "res1", null, new BasicResolver()); + JingleSession js2 = new OutgoingJingleSession(getConnection(1), "res1", null, new BasicResolver()); + JingleSession js3 = new OutgoingJingleSession(getConnection(2), "res2", null, new BasicResolver()); + + System.out.println(js1.getSid()); + System.out.println(js2.getSid()); + + js1.setInitiator("js1"); + js2.setInitiator("js1"); + js1.setSid("10"); + js2.setSid("10"); + + assertEquals(js1, js2); + assertEquals(js2, js1); + + assertFalse(js1.equals(js3)); + } + + public void testGetInstanceFor() { + String ini1 = "initiator1"; + String sid1 = "sid1"; + String ini2 = "initiator2"; + String sid2 = "sid2"; + + JingleSession js1 = new OutgoingJingleSession(getConnection(0), sid1, null, new BasicResolver()); + JingleSession js2 = new OutgoingJingleSession(getConnection(1), sid2, null, new BasicResolver()); + + // For a packet, we should be able to get a session that handles that... + assertNotNull(JingleSession.getInstanceFor(getConnection(0))); + assertNotNull(JingleSession.getInstanceFor(getConnection(1))); + + assertEquals(JingleSession.getInstanceFor(getConnection(0)), js1); + assertEquals(JingleSession.getInstanceFor(getConnection(1)), js2); + } + + protected int getMaxConnections() { + return 3; + } +} diff --git a/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleSupportTests.java b/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleSupportTests.java new file mode 100644 index 000000000..03f8e68af --- /dev/null +++ b/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleSupportTests.java @@ -0,0 +1,74 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright (C) 2002-2006 Jive Software. All rights reserved. + * ==================================================================== + * The Jive Software License (based on Apache Software License, Version 1.1) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by + * Jive Software (http://www.jivesoftware.com)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Smack" and "Jive Software" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please + * contact webmaster@jivesoftware.com. + * + * 5. Products derived from this software may not be called "Smack", + * nor may "Smack" appear in their name, without prior written + * permission of Jive Software. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL JIVE SOFTWARE OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + */ + +package org.jivesoftware.smackx.jingle; + +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * Test suite that runs all the Jingle support tests + * + * @author Alvaro Saurin + */ +public class JingleSupportTests { + + public static Test suite() { + TestSuite suite = new TestSuite("High and low level API tests for Jingle support"); + + // $JUnit-BEGIN$ + suite.addTest(new TestSuite(JingleManagerTest.class)); + // $JUnit-END$ + + return suite; + } +} diff --git a/jingle/extension/test/org/jivesoftware/smackx/jingle/PayloadTypeTest.java b/jingle/extension/test/org/jivesoftware/smackx/jingle/PayloadTypeTest.java new file mode 100644 index 000000000..647e37bb8 --- /dev/null +++ b/jingle/extension/test/org/jivesoftware/smackx/jingle/PayloadTypeTest.java @@ -0,0 +1,89 @@ +package org.jivesoftware.smackx.jingle; + +import java.util.ArrayList; + +import org.jivesoftware.smack.test.SmackTestCase; +import org.jivesoftware.smackx.jingle.media.PayloadType; + +public class PayloadTypeTest extends SmackTestCase { + + public PayloadTypeTest(final String arg0) { + super(arg0); + } + + public void testEqualsObject() { + PayloadType p1 = new PayloadType(0, "pt1", 2); + PayloadType p2 = new PayloadType(0, "pt1", 2); + assertTrue(p1.equals(p2)); + } + + /** + * Test for the difference of payloads. + */ + public void testDifference() { + ArrayList set1 = new ArrayList(); + ArrayList set2 = new ArrayList(); + + PayloadType.Audio common1 = new PayloadType.Audio(34, "supercodec-1", 2, 14000); + PayloadType.Audio common2 = new PayloadType.Audio(56, "supercodec-2", 1, 44000); + + set1.add(common1); + set1.add(common2); + set1.add(new PayloadType.Audio(36, "supercodec-3", 2, 28000)); + set1.add(new PayloadType.Audio(45, "supercodec-4", 1, 98000)); + + set2.add(new PayloadType.Audio(27, "supercodec-3", 2, 28000)); + set2.add(common2); + set2.add(new PayloadType.Audio(32, "supercodec-4", 1, 98000)); + set2.add(common1); + + // Get the difference + ArrayList commonSet = new ArrayList(); + commonSet.addAll(set1); + commonSet.retainAll(set2); + + assertTrue(commonSet.size() == 2); + System.out.println("Codec " + ((PayloadType.Audio)commonSet.get(0)).getId()); + System.out.println("Codec " + ((PayloadType.Audio)commonSet.get(1)).getId()); + + assertTrue(commonSet.contains(common1)); + assertTrue(commonSet.contains(common2)); + } + + /** + * Test for the difference of payloads when we are handling the same sets. + */ + public void testDifferenceSameSet() { + ArrayList set1 = new ArrayList(); + ArrayList set2 = new ArrayList(); + + PayloadType.Audio common1 = new PayloadType.Audio(34, "supercodec-1", 2, 14000); + PayloadType.Audio common2 = new PayloadType.Audio(56, "supercodec-2", 1, 44000); + PayloadType.Audio common3 = new PayloadType.Audio(0, "supercodec-3", 1, 44000); + PayloadType.Audio common4 = new PayloadType.Audio(120, "supercodec-4", 2, 66060); + + set1.add(common1); + set1.add(common2); + set1.add(common3); + set1.add(common4); + + set2.add(common1); + set2.add(common2); + set2.add(common3); + set2.add(common4); + + // Get the difference + ArrayList commonSet = new ArrayList(); + commonSet.addAll(set1); + commonSet.retainAll(set2); + + assertTrue(commonSet.size() == 4); + assertTrue(commonSet.contains(common1)); + assertTrue(commonSet.contains(common2)); + } + + protected int getMaxConnections() { + return 0; + } + +} diff --git a/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/BasicResolverTest.java b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/BasicResolverTest.java new file mode 100644 index 000000000..16fc889db --- /dev/null +++ b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/BasicResolverTest.java @@ -0,0 +1,101 @@ +package org.jivesoftware.smackx.jingle.nat; + +import org.jivesoftware.smack.test.SmackTestCase; + +public class BasicResolverTest extends SmackTestCase { + + private int counter; + + private final Object mutex = new Object(); + + public BasicResolverTest(String arg) { + super(arg); + } + + // Counter management + + private void resetCounter() { + synchronized (mutex) { + counter = 0; + } + } + + private void incCounter() { + synchronized (mutex) { + counter++; + } + } + + private int valCounter() { + int val; + synchronized (mutex) { + val = counter; + } + return val; + } + + public void testCheckValidHostname() { + String validHostname = new String("slashdot.org"); + BasicResolver br = new BasicResolver(); + TransportCandidate tc = new TransportCandidate.Fixed(validHostname, 0); + + resetCounter(); + + tc.addListener(new TransportResolverListener.Checker() { + public void candidateChecked(TransportCandidate cand, boolean result) { + if(result == true) { + System.out.println(cand.getIp() + " is reachable (as expected)"); + incCounter(); + } + } + + public void candidateChecking(TransportCandidate cand) { + + } + }); + + tc.check(); + + try { + Thread.sleep(TransportResolver.CHECK_TIMEOUT); + } catch (Exception e) { + } + + assertTrue(valCounter() > 0); + } + + public void testCheckInvalidHostname() { + String invalidHostname = new String("camupilosupino.org"); + BasicResolver br = new BasicResolver(); + TransportCandidate tc = new TransportCandidate.Fixed(invalidHostname, 0); + + resetCounter(); + + tc.addListener(new TransportResolverListener.Checker() { + public void candidateChecked(TransportCandidate cand, boolean result) { + if(result == false) { + System.out.println(cand.getIp() + " is _not_ reachable (as expected)"); + incCounter(); + } + } + + public void candidateChecking(TransportCandidate cand) { + } + }); + + tc.check(); + + try { + Thread.sleep(TransportResolver.CHECK_TIMEOUT); + } catch (Exception e) { + } + + assertTrue(valCounter() > 0); + } + + + protected int getMaxConnections() { + return 0; + } + +} diff --git a/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/BridgedResolverTest.java b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/BridgedResolverTest.java new file mode 100644 index 000000000..577fc5d09 --- /dev/null +++ b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/BridgedResolverTest.java @@ -0,0 +1,172 @@ +package org.jivesoftware.smackx.jingle.nat; + +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.test.SmackTestCase; +import org.jivesoftware.smackx.jingle.IncomingJingleSession; +import org.jivesoftware.smackx.jingle.JingleManager; +import org.jivesoftware.smackx.jingle.JingleSessionRequest; +import org.jivesoftware.smackx.jingle.OutgoingJingleSession; +import org.jivesoftware.smackx.jingle.listeners.JingleSessionRequestListener; +import org.jivesoftware.smackx.jingle.media.JingleMediaManager; +import org.jivesoftware.smackx.jingle.media.JingleMediaSession; +import org.jivesoftware.smackx.jingle.media.PayloadType; + +public class BridgedResolverTest extends SmackTestCase { + + private int counter; + + private final Object mutex = new Object(); + + public BridgedResolverTest(String arg) { + super(arg); + } + + // Counter management + + private void resetCounter() { + synchronized (mutex) { + counter = 0; + } + } + + private void incCounter() { + synchronized (mutex) { + counter++; + } + } + + private int valCounter() { + int val; + synchronized (mutex) { + val = counter; + } + return val; + } + + public void testCheckService() { + assertTrue(RTPBridge.serviceAvailable(getConnection(0))); + } + + public void testGetBridge() { + + resetCounter(); + + RTPBridge rtpBridge = RTPBridge.getRTPBridge(getConnection(0), "001"); + + System.out.println(rtpBridge.getIp() + " portA:" + rtpBridge.getPortA() + " portB:" + rtpBridge.getPortB()); + + if (rtpBridge != null) { + if (rtpBridge.getIp() != null) incCounter(); + if (rtpBridge.getPortA() != -1) incCounter(); + if (rtpBridge.getPortB() != -1) incCounter(); + } + + assertTrue(valCounter() == 3); + } + + public void testFullBridge() { + resetCounter(); + + try { + + //XMPPConnection.DEBUG_ENABLED = true; + + XMPPConnection x0 = new XMPPConnection("thiago"); + XMPPConnection x1 = new XMPPConnection("thiago"); + + x0.connect(); + x0.login("barata7", "barata7"); + x1.connect(); + x1.login("barata6", "barata6"); + + final JingleManager jm0 = new JingleManager( + x0, new BridgedResolver(x0)); + final JingleManager jm1 = new JingleManager( + x1, new BridgedResolver(x1)); + + JingleMediaManager jingleMediaManager = new JingleMediaManager() { + // Media Session Implementation + public JingleMediaSession createMediaSession(final PayloadType payloadType, final TransportCandidate remote, final TransportCandidate local) { + return new JingleMediaSession(payloadType, remote, local) { + + public void initialize() { + + } + + public void startTrasmit() { + incCounter(); + + System.out.print("IPs:"); + System.out.println(local.getSymmetric().getIp()); + System.out.println(local.getIp()); + + System.out.println("Transmit"); + } + + public void startReceive() { + incCounter(); + System.out.println("Receive"); + } + + public void setTrasmit(boolean active) { + } + + public void stopTrasmit() { + incCounter(); + System.out.println("Stop Transmit"); + } + + public void stopReceive() { + incCounter(); + System.out.println("Stop Receive"); + } + }; + } + }; + + jingleMediaManager.addPayloadType(new PayloadType.Audio(3, "GSM", 1, 16000)); + + jm0.setMediaManager(jingleMediaManager); + jm1.setMediaManager(jingleMediaManager); + + jm1.addJingleSessionRequestListener(new JingleSessionRequestListener() { + public void sessionRequested(final JingleSessionRequest request) { + + try { + + IncomingJingleSession session = request.accept(jm1.getMediaManager().getPayloads()); + + session.start(request); + } catch (XMPPException e) { + e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. + } + + } + }); + + OutgoingJingleSession js0 = jm0.createOutgoingJingleSession("barata6@thiago/Smack"); + + js0.start(); + + Thread.sleep(10000); + js0.terminate(); + + Thread.sleep(3000); + + System.out.println(valCounter()); + + assertTrue(valCounter() == 8); + //Thread.sleep(15000); + + } catch (Exception e) { + e.printStackTrace(); + } + + } + + protected int getMaxConnections() { + return 1; + } + +} diff --git a/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/STUNResolverTest.java b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/STUNResolverTest.java new file mode 100644 index 000000000..ec75f5419 --- /dev/null +++ b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/STUNResolverTest.java @@ -0,0 +1,342 @@ +package org.jivesoftware.smackx.jingle.nat; + +import de.javawi.jstun.test.demo.ice.Candidate; +import de.javawi.jstun.test.demo.ice.ICENegociator; +import de.javawi.jstun.util.UtilityException; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.test.SmackTestCase; +import org.jivesoftware.smackx.jingle.*; +import org.jivesoftware.smackx.jingle.listeners.JingleSessionListener; +import org.jivesoftware.smackx.jingle.listeners.JingleSessionRequestListener; +import org.jivesoftware.smackx.jingle.media.PayloadType; + +import java.net.UnknownHostException; +import java.util.ArrayList; + +/** + * Test the STUN IP resolver. + * + * @author alvaro + */ +public class STUNResolverTest extends SmackTestCase { + + // Counter management + + public STUNResolverTest(final String arg) { + super(arg); + } + + private int counter; + + private final Object mutex = new Object(); + + private void resetCounter() { + synchronized (mutex) { + counter = 0; + } + } + + private void incCounter() { + synchronized (mutex) { + counter++; + } + } + + private int valCounter() { + int val; + synchronized (mutex) { + val = counter; + } + return val; + } + + /** + * Test for getPreferredCandidate() + * + * @throws Exception + */ + public void testGetPreferredCandidate() throws Exception { + int highestPref = 100; + + TransportCandidate cand1 = new TransportCandidate.Ice("192.168.2.1", 3, 2, + "password", 3468, "username1", 1); + TransportCandidate cand2 = new TransportCandidate.Ice("192.168.5.1", 2, 10, + "password", 3469, "username2", 15); + TransportCandidate candH = new TransportCandidate.Ice("192.168.2.11", 1, 2, + "password", 3468, "usernameH", highestPref); + TransportCandidate cand3 = new TransportCandidate.Ice("192.168.2.10", 2, 10, + "password", 3469, "username3", 2); + TransportCandidate cand4 = new TransportCandidate.Ice("192.168.4.1", 3, 2, + "password", 3468, "username4", 78); + + STUNResolver stunResolver = new STUNResolver() { + }; + stunResolver.addCandidate(cand1); + stunResolver.addCandidate(cand2); + stunResolver.addCandidate(candH); + stunResolver.addCandidate(cand3); + stunResolver.addCandidate(cand4); + + assertEquals(stunResolver.getPreferredCandidate(), candH); + } + + /** + * Test for getPreferredCandidate() + * + * @throws Exception + */ + public void testGetPreferredCandidateICE() throws Exception { + int highestPref = 100; + + TransportCandidate cand1 = new TransportCandidate.Ice("192.168.2.1", 3, 2, + "password", 3468, "username1", 1); + TransportCandidate cand2 = new TransportCandidate.Ice("192.168.5.1", 2, 10, + "password", 3469, "username2", 15); + TransportCandidate candH = new TransportCandidate.Ice("192.168.2.11", 1, 2, + "password", 3468, "usernameH", highestPref); + TransportCandidate cand3 = new TransportCandidate.Ice("192.168.2.10", 2, 10, + "password", 3469, "username3", 2); + TransportCandidate cand4 = new TransportCandidate.Ice("192.168.4.1", 3, 2, + "password", 3468, "username4", 78); + + ICEResolver iceResolver = new ICEResolver() { + }; + iceResolver.addCandidate(cand1); + iceResolver.addCandidate(cand2); + iceResolver.addCandidate(candH); + iceResolver.addCandidate(cand3); + iceResolver.addCandidate(cand4); + + assertEquals(iceResolver.getPreferredCandidate(), candH); + } + + /** + * Test priority generated by STUN lib + * + * @throws Exception + */ + public void testICEPriority() throws Exception { + + String first = ""; + + for (int i = 0; i < 100; i++) { + + ICENegociator cc = new ICENegociator((short) 1); + // gather candidates + cc.gatherCandidateAddresses(); + // priorize candidates + cc.prioritizeCandidates(); + // get SortedCandidates + //List sortedCandidates = cc.getSortedCandidates(); + + for (Candidate candidate : cc.getSortedCandidates()) + try { + TransportCandidate transportCandidate = new TransportCandidate.Ice(candidate.getAddress().getInetAddress().getHostAddress(), 1, candidate.getNetwork(), "1", candidate.getPort(), "1", candidate.getPriority()); + transportCandidate.setLocalIp(candidate.getBase().getAddress().getInetAddress().getHostAddress()); + System.out.println("C: " + candidate.getAddress().getInetAddress() + "|" + candidate.getBase().getAddress().getInetAddress() + " p:" + candidate.getPriority()); + } catch (UtilityException e) { + e.printStackTrace(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } + Candidate candidate = cc.getSortedCandidates().get(0); + String temp = "C: " + candidate.getAddress().getInetAddress() + "|" + candidate.getBase().getAddress().getInetAddress() + " p:" + candidate.getPriority(); + if (first.equals("")) + first = temp; + assertEquals(first, temp); + first = temp; + } + } + + /** + * Test for loadSTUNServers() + * + * @throws Exception + */ + public void testLoadSTUNServers() throws Exception { + STUNResolver stunResolver = new STUNResolver() { + }; + ArrayList stunServers = stunResolver.loadSTUNServers(); + + assertTrue(stunServers.size() > 0); + System.out.println(stunServers.size() + " servers loaded"); + } + + /** + * Test for resolve() + * + * @throws Exception + */ + public void testResolve() throws Exception { + + final STUNResolver stunResolver = new STUNResolver() { + }; + + stunResolver.addListener(new TransportResolverListener.Resolver() { + + public void candidateAdded(final TransportCandidate cand) { + incCounter(); + + String addr = cand.getIp(); + int port = cand.getPort(); + + System.out.println("Addr: " + addr + " port:" + port); + + } + + public void init() { + System.out.println("Resolution started"); + } + + public void end() { + System.out.println("Resolution finished"); + } + }); + + try { + stunResolver.initialize(); + Thread.sleep(55000); + assertTrue(valCounter() > 0); + stunResolver.resolve(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + /** + * Generate a list of payload types + * + * @return A testing list + */ + private ArrayList getTestPayloads1() { + ArrayList result = new ArrayList(); + + result.add(new PayloadType.Audio(34, "supercodec-1", 2, 14000)); + result.add(new PayloadType.Audio(56, "supercodec-2", 1, 44000)); + result.add(new PayloadType.Audio(36, "supercodec-3", 2, 28000)); + result.add(new PayloadType.Audio(45, "supercodec-4", 1, 98000)); + + return result; + } + + private ArrayList getTestPayloads2() { + ArrayList result = new ArrayList(); + + result.add(new PayloadType.Audio(27, "supercodec-3", 2, 28000)); + result.add(new PayloadType.Audio(56, "supercodec-2", 1, 44000)); + result.add(new PayloadType.Audio(32, "supercodec-4", 1, 98000)); + result.add(new PayloadType.Audio(34, "supercodec-1", 2, 14000)); + + return result; + } + + /** + * This is a simple test where the user_2 rejects the Jingle session. + */ + public void testSTUNJingleSession() { + + resetCounter(); + + try { + TransportResolver tr1 = new STUNResolver() { + }; + TransportResolver tr2 = new STUNResolver() { + }; + + // Explicit resolution + tr1.resolve(); + tr2.resolve(); + + final JingleManager man0 = new JingleManager(getConnection(0), tr1); + final JingleManager man1 = new JingleManager(getConnection(1), tr2); + + man1.addJingleSessionRequestListener(new JingleSessionRequestListener() { + /** + * Called when a new session request is detected + */ + public void sessionRequested(final JingleSessionRequest request) { + System.out.println("Session request detected, from " + + request.getFrom() + ": accepting."); + + // We accept the request + IncomingJingleSession session1; + try { + session1 = request.accept(getTestPayloads2()); + session1.addListener(new JingleSessionListener() { + public void sessionClosed(String reason, JingleSession jingleSession) { + } + + public void sessionClosedOnError(XMPPException e, JingleSession jingleSession) { + } + + public void sessionDeclined(String reason, JingleSession jingleSession) { + } + + public void sessionEstablished(PayloadType pt, + TransportCandidate rc, TransportCandidate lc, JingleSession jingleSession) { + incCounter(); + System.out + .println("Responder: the session is fully established."); + System.out.println("+ Payload Type: " + pt.getId()); + System.out.println("+ Local IP/port: " + lc.getIp() + ":" + + lc.getPort()); + System.out.println("+ Remote IP/port: " + rc.getIp() + ":" + + rc.getPort()); + } + + public void sessionRedirected(String redirection, JingleSession jingleSession) { + } + }); + session1.start(request); + } catch (XMPPException e) { + e.printStackTrace(); + } + } + }); + + // Session 0 starts a request + System.out.println("Starting session request, to " + getFullJID(1) + "..."); + OutgoingJingleSession session0 = man0.createOutgoingJingleSession( + getFullJID(1), getTestPayloads1()); + + session0.addListener(new JingleSessionListener() { + public void sessionClosed(String reason, JingleSession jingleSession) { + } + + public void sessionClosedOnError(XMPPException e, JingleSession jingleSession) { + } + + public void sessionDeclined(String reason, JingleSession jingleSession) { + } + + public void sessionEstablished(PayloadType pt, + TransportCandidate rc, TransportCandidate lc, JingleSession jingleSession) { + incCounter(); + System.out.println("Initiator: the session is fully established."); + System.out.println("+ Payload Type: " + pt.getId()); + System.out.println("+ Local IP/port: " + lc.getIp() + ":" + + lc.getPort()); + System.out.println("+ Remote IP/port: " + rc.getIp() + ":" + + rc.getPort()); + } + + public void sessionRedirected(String redirection, JingleSession jingleSession) { + } + }); + session0.start(null); + + Thread.sleep(60000); + + assertTrue(valCounter() == 2); + + } catch (Exception e) { + e.printStackTrace(); + fail("An error occured with Jingle"); + } + } + + protected int getMaxConnections() { + return 2; + } +} diff --git a/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/TransportCandidateTest.java b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/TransportCandidateTest.java new file mode 100644 index 000000000..2aee30e70 --- /dev/null +++ b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/TransportCandidateTest.java @@ -0,0 +1,59 @@ +package org.jivesoftware.smackx.jingle.nat; + +import java.util.ArrayList; +import java.util.Collections; +import org.jivesoftware.smack.test.SmackTestCase; + +public class TransportCandidateTest extends SmackTestCase { + + public TransportCandidateTest(final String arg0) { + super(arg0); + } + + /** + * Test for equals() + */ + public void testEqualsObject() { + TransportCandidate cand1 = new TransportCandidate.Ice("192.168.2.1", 1, 2, + "password", 3468, "username", 25); + TransportCandidate cand2 = new TransportCandidate.Ice("192.168.2.1", 1, 2, + "password", 3468, "username", 25); + TransportCandidate cand3 = new TransportCandidate.Ice("192.168.2.1", 1, 2, + "password", 3469, "username", 25); + + assertEquals(cand1, cand2); + assertFalse(cand1.equals(cand3)); + } + + /** + * Test for compareTo() + */ + public void testCompareTo() { + int highestPref = 100; + + TransportCandidate cand1 = new TransportCandidate.Ice("192.168.2.1", 3, 2, + "password", 3468, "username", 1); + TransportCandidate cand2 = new TransportCandidate.Ice("192.168.5.1", 2, 10, + "password", 3469, "username", 15); + TransportCandidate candH = new TransportCandidate.Ice("192.168.2.1", 1, 2, + "password", 3468, "username", highestPref); + TransportCandidate cand3 = new TransportCandidate.Ice("192.168.2.10", 2, 10, + "password", 3469, "username", 2); + TransportCandidate cand4 = new TransportCandidate.Ice("192.168.4.1", 3, 2, + "password", 3468, "username", 78); + + ArrayList candList = new ArrayList(); + candList.add(cand1); + candList.add(cand2); + candList.add(candH); + candList.add(cand3); + candList.add(cand4); + + Collections.sort(candList); + assertEquals(candList.get(candList.size() - 1), candH); + } + + protected int getMaxConnections() { + return 0; + } +} diff --git a/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/TransportResolverTest.java b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/TransportResolverTest.java new file mode 100644 index 000000000..a7fc45cb4 --- /dev/null +++ b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/TransportResolverTest.java @@ -0,0 +1,50 @@ +package org.jivesoftware.smackx.jingle.nat; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.test.SmackTestCase; + +public class TransportResolverTest extends SmackTestCase { + + public TransportResolverTest(final String arg) { + super(arg); + } + + public void testIsResolving() { + final TransportResolver tr = new BasicResolver(); + + tr.addListener( + new TransportResolverListener.Resolver() { + public void candidateAdded(final TransportCandidate cand) { + System.out.println("candidateAdded() called."); + assertTrue(tr.isResolving() || (!tr.isResolving() && tr.isResolved())); + } + + public void end() { + System.out.println("end() called."); + assertFalse(tr.isResolving()); + assertTrue(tr.isResolved()); + } + + public void init() { + System.out.println("init() called."); + assertTrue(tr.isResolving()); + assertFalse(tr.isResolved()); + } + }); + + assertFalse(tr.isResolving()); + assertFalse(tr.isResolved()); + + try { + tr.resolve(); + } catch (XMPPException e) { + e.printStackTrace(); + fail("Error resolving"); + } + } + + protected int getMaxConnections() { + return 0; + } + +} diff --git a/jingle/extension/test/org/jivesoftware/smackx/provider/JingleProviderTest.java b/jingle/extension/test/org/jivesoftware/smackx/provider/JingleProviderTest.java new file mode 100644 index 000000000..a54732410 --- /dev/null +++ b/jingle/extension/test/org/jivesoftware/smackx/provider/JingleProviderTest.java @@ -0,0 +1,112 @@ +package org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.filter.PacketTypeFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smack.provider.ProviderManager; +import org.jivesoftware.smack.test.SmackTestCase; +import org.jivesoftware.smackx.packet.Jingle; + +public class JingleProviderTest extends SmackTestCase { + + public JingleProviderTest(final String name) { + super(name); + } + + public void testProviderManager() { + IQProvider iqProv; + String elementNamee = Jingle.getElementName(); + String nameSpace = Jingle.getNamespace(); + + System.out.println("Testing if the Jingle IQ provider is registered..."); + + // Verify that the Jingle IQProvider is registered. + iqProv = (IQProvider)ProviderManager.getInstance().getIQProvider(elementNamee, nameSpace); + + assertNotNull(iqProv); + } + + /** + * Test for parsing a Jingle + */ + public void testParseIQSimple() { + + // Create a dummy packet for testing... + IQfake iqSent = new IQfake ( + " " + + " " + + " " + + " " + + ""); + + iqSent.setTo(getFullJID(0)); + iqSent.setFrom(getFullJID(0)); + iqSent.setType(IQ.Type.GET); + + // Create a filter and a collector... + PacketFilter filter = new PacketTypeFilter(IQ.class); + PacketCollector collector = getConnection(0).createPacketCollector(filter); + + System.out.println("Testing if a Jingle IQ can be sent and received..."); + + // Send the iq packet with an invalid namespace + getConnection(0).sendPacket(iqSent); + + // Receive the packet + IQ iqReceived = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + + // Stop queuing results + collector.cancel(); + + if (iqReceived == null) { + fail("No response from server"); + } + else if (iqReceived.getType() == IQ.Type.ERROR) { + fail("The server did reply with an error packet: " + iqReceived.getError().getCode()); + } + else { + assertTrue(iqReceived instanceof Jingle); + + Jingle jin = (Jingle) iqReceived; + + System.out.println("Sent: " + iqSent.toXML()); + System.out.println("Received: " + jin.toXML()); + } + } + + /** + * Simple class for testing an IQ... + * @author Alvaro Saurin + */ + private class IQfake extends IQ { + private String s; + + public IQfake(final String s) { + super(); + this.s = s; + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append(s); + return buf.toString(); + } + } + + + protected int getMaxConnections() { + return 2; + } + +} diff --git a/jingle/media/build/lib/Speex.jar b/jingle/media/build/lib/Speex.jar new file mode 100644 index 000000000..f8abcbb0b Binary files /dev/null and b/jingle/media/build/lib/Speex.jar differ diff --git a/jingle/media/build/lib/commons-logging-1.1.jar b/jingle/media/build/lib/commons-logging-1.1.jar new file mode 100644 index 000000000..2ff9bbd90 Binary files /dev/null and b/jingle/media/build/lib/commons-logging-1.1.jar differ diff --git a/jingle/media/build/lib/commons-logging-adapters-1.1.jar b/jingle/media/build/lib/commons-logging-adapters-1.1.jar new file mode 100644 index 000000000..6eec9a525 Binary files /dev/null and b/jingle/media/build/lib/commons-logging-adapters-1.1.jar differ diff --git a/jingle/media/build/lib/commons-logging-api-1.1.jar b/jingle/media/build/lib/commons-logging-api-1.1.jar new file mode 100644 index 000000000..d1abcbb47 Binary files /dev/null and b/jingle/media/build/lib/commons-logging-api-1.1.jar differ diff --git a/jingle/media/build/lib/jmf.jar b/jingle/media/build/lib/jmf.jar new file mode 100644 index 000000000..556b508ef Binary files /dev/null and b/jingle/media/build/lib/jmf.jar differ diff --git a/jingle/media/build/lib/jspeex-0.9.7-jfcom.jar b/jingle/media/build/lib/jspeex-0.9.7-jfcom.jar new file mode 100644 index 000000000..f2631b1f4 Binary files /dev/null and b/jingle/media/build/lib/jspeex-0.9.7-jfcom.jar differ diff --git a/jingle/media/build/lib/windows/jmacm.dll b/jingle/media/build/lib/windows/jmacm.dll new file mode 100644 index 000000000..3edd7ebf2 Binary files /dev/null and b/jingle/media/build/lib/windows/jmacm.dll differ diff --git a/jingle/media/build/lib/windows/jmam.dll b/jingle/media/build/lib/windows/jmam.dll new file mode 100644 index 000000000..46c0ddc2a Binary files /dev/null and b/jingle/media/build/lib/windows/jmam.dll differ diff --git a/jingle/media/build/lib/windows/jmcvid.dll b/jingle/media/build/lib/windows/jmcvid.dll new file mode 100644 index 000000000..fad506adf Binary files /dev/null and b/jingle/media/build/lib/windows/jmcvid.dll differ diff --git a/jingle/media/build/lib/windows/jmdaud.dll b/jingle/media/build/lib/windows/jmdaud.dll new file mode 100644 index 000000000..61a18d633 Binary files /dev/null and b/jingle/media/build/lib/windows/jmdaud.dll differ diff --git a/jingle/media/build/lib/windows/jmdaudc.dll b/jingle/media/build/lib/windows/jmdaudc.dll new file mode 100644 index 000000000..5847317cd Binary files /dev/null and b/jingle/media/build/lib/windows/jmdaudc.dll differ diff --git a/jingle/media/build/lib/windows/jmddraw.dll b/jingle/media/build/lib/windows/jmddraw.dll new file mode 100644 index 000000000..4001890dc Binary files /dev/null and b/jingle/media/build/lib/windows/jmddraw.dll differ diff --git a/jingle/media/build/lib/windows/jmfjawt.dll b/jingle/media/build/lib/windows/jmfjawt.dll new file mode 100644 index 000000000..b44df82af Binary files /dev/null and b/jingle/media/build/lib/windows/jmfjawt.dll differ diff --git a/jingle/media/build/lib/windows/jmg723.dll b/jingle/media/build/lib/windows/jmg723.dll new file mode 100644 index 000000000..d1172fd21 Binary files /dev/null and b/jingle/media/build/lib/windows/jmg723.dll differ diff --git a/jingle/media/build/lib/windows/jmgdi.dll b/jingle/media/build/lib/windows/jmgdi.dll new file mode 100644 index 000000000..a68e16cf9 Binary files /dev/null and b/jingle/media/build/lib/windows/jmgdi.dll differ diff --git a/jingle/media/build/lib/windows/jmgsm.dll b/jingle/media/build/lib/windows/jmgsm.dll new file mode 100644 index 000000000..4317ffb55 Binary files /dev/null and b/jingle/media/build/lib/windows/jmgsm.dll differ diff --git a/jingle/media/build/lib/windows/jmh263enc.dll b/jingle/media/build/lib/windows/jmh263enc.dll new file mode 100644 index 000000000..8c25686bf Binary files /dev/null and b/jingle/media/build/lib/windows/jmh263enc.dll differ diff --git a/jingle/media/build/lib/windows/jmjpeg.dll b/jingle/media/build/lib/windows/jmjpeg.dll new file mode 100644 index 000000000..3a42e9261 Binary files /dev/null and b/jingle/media/build/lib/windows/jmjpeg.dll differ diff --git a/jingle/media/build/lib/windows/jmmci.dll b/jingle/media/build/lib/windows/jmmci.dll new file mode 100644 index 000000000..e17df4082 Binary files /dev/null and b/jingle/media/build/lib/windows/jmmci.dll differ diff --git a/jingle/media/build/lib/windows/jmmpa.dll b/jingle/media/build/lib/windows/jmmpa.dll new file mode 100644 index 000000000..5356ba37a Binary files /dev/null and b/jingle/media/build/lib/windows/jmmpa.dll differ diff --git a/jingle/media/build/lib/windows/jmmpegv.dll b/jingle/media/build/lib/windows/jmmpegv.dll new file mode 100644 index 000000000..d44f1889b Binary files /dev/null and b/jingle/media/build/lib/windows/jmmpegv.dll differ diff --git a/jingle/media/build/lib/windows/jmutil.dll b/jingle/media/build/lib/windows/jmutil.dll new file mode 100644 index 000000000..c22b26f9b Binary files /dev/null and b/jingle/media/build/lib/windows/jmutil.dll differ diff --git a/jingle/media/build/lib/windows/jmvcm.dll b/jingle/media/build/lib/windows/jmvcm.dll new file mode 100644 index 000000000..45711f019 Binary files /dev/null and b/jingle/media/build/lib/windows/jmvcm.dll differ diff --git a/jingle/media/build/lib/windows/jmvfw.dll b/jingle/media/build/lib/windows/jmvfw.dll new file mode 100644 index 000000000..d77e7cde7 Binary files /dev/null and b/jingle/media/build/lib/windows/jmvfw.dll differ diff --git a/jingle/media/build/lib/windows/jmvh263.dll b/jingle/media/build/lib/windows/jmvh263.dll new file mode 100644 index 000000000..e554d0c19 Binary files /dev/null and b/jingle/media/build/lib/windows/jmvh263.dll differ diff --git a/jingle/media/build/projects/JingleMedia.iml b/jingle/media/build/projects/JingleMedia.iml new file mode 100644 index 000000000..a51db05cc --- /dev/null +++ b/jingle/media/build/projects/JingleMedia.iml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jingle/media/source/org/jivesoftware/demo/Demo.java b/jingle/media/source/org/jivesoftware/demo/Demo.java new file mode 100644 index 000000000..c170cf101 --- /dev/null +++ b/jingle/media/source/org/jivesoftware/demo/Demo.java @@ -0,0 +1,157 @@ +package org.jivesoftware.demo; + +import org.jivesoftware.jingleaudio.jmf.JmfMediaManager; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.jingle.IncomingJingleSession; +import org.jivesoftware.smackx.jingle.JingleManager; +import org.jivesoftware.smackx.jingle.JingleSessionRequest; +import org.jivesoftware.smackx.jingle.OutgoingJingleSession; +import org.jivesoftware.smackx.jingle.listeners.JingleSessionRequestListener; +import org.jivesoftware.smackx.jingle.nat.BridgedTransportManager; +import org.jivesoftware.smackx.jingle.nat.JingleTransportManager; +import org.jivesoftware.smackx.jingle.nat.RTPBridge; +import org.jivesoftware.smackx.jingle.nat.STUNTransportManager; + +import javax.swing.*; +import java.awt.event.ActionEvent; + +/** + * $RCSfile$ + * $Revision: $ + * $Date: 28/12/2006 + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +public class Demo extends JFrame { + + private JingleTransportManager transportManager = null; + private XMPPConnection xmppConnection = null; + + private String server = null; + private String user = null; + private String pass = null; + + private JingleManager jm = null; + private IncomingJingleSession incoming = null; + private OutgoingJingleSession outgoing = null; + + private JTextField jid = new JTextField(30); + + public Demo(String server, String user, String pass) { + + this.server = server; + this.user = user; + this.pass = pass; + + xmppConnection = new XMPPConnection(server); + try { + xmppConnection.connect(); + xmppConnection.login(user, pass); + initialize(); + } catch (XMPPException e) { + e.printStackTrace(); + } + } + + public void initialize() { + if (RTPBridge.serviceAvailable(xmppConnection)) + transportManager = new BridgedTransportManager(xmppConnection); + else + transportManager = new STUNTransportManager(); + + jm = new JingleManager(xmppConnection, transportManager, new JmfMediaManager()); + + if (transportManager instanceof BridgedTransportManager) + jm.addCreationListener((BridgedTransportManager) transportManager); + + jm.addJingleSessionRequestListener(new JingleSessionRequestListener() { + public void sessionRequested(JingleSessionRequest request) { + + if (incoming != null) + return; + + try { + // Accept the call + incoming = request.accept(); + + // Start the call + incoming.start(); + } + catch (XMPPException e) { + e.printStackTrace(); + } + + } + }); + createGUI(); + } + + public void createGUI() { + + JPanel jPanel = new JPanel(); + + jPanel.add(jid); + + jPanel.add(new JButton(new AbstractAction("Call") { + public void actionPerformed(ActionEvent e) { + if (outgoing != null) return; + try { + outgoing = jm.createOutgoingJingleSession(jid.getText()); + } catch (XMPPException e1) { + e1.printStackTrace(); + } + } + })); + + jPanel.add(new JButton(new AbstractAction("Hangup") { + public void actionPerformed(ActionEvent e) { + if (outgoing != null) + try { + outgoing.terminate(); + } catch (XMPPException e1) { + e1.printStackTrace(); + } finally { + outgoing = null; + } + if (incoming != null) + try { + incoming.terminate(); + } catch (XMPPException e1) { + e1.printStackTrace(); + } finally { + incoming = null; + } + } + })); + + this.add(jPanel); + + } + + public static void main(String args[]) { + + Demo demo = null; + + if (args.length > 2) { + demo = new Demo(args[0], args[1], args[2]); + demo.pack(); + demo.setVisible(true); + demo.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + } + + } + +} diff --git a/jingle/media/source/org/jivesoftware/jingleaudio/jmf/AudioChannel.java b/jingle/media/source/org/jivesoftware/jingleaudio/jmf/AudioChannel.java new file mode 100644 index 000000000..8a3074590 --- /dev/null +++ b/jingle/media/source/org/jivesoftware/jingleaudio/jmf/AudioChannel.java @@ -0,0 +1,439 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: 08/11/2006 + *

+ * Copyright 2003-2006 Jive Software. + *

+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.jingleaudio.jmf; + +import javax.media.*; +import javax.media.control.TrackControl; +import javax.media.format.AudioFormat; +import javax.media.protocol.ContentDescriptor; +import javax.media.protocol.DataSource; +import javax.media.protocol.PushBufferDataSource; +import javax.media.protocol.PushBufferStream; +import javax.media.rtp.RTPManager; +import javax.media.rtp.SendStream; +import javax.media.rtp.SessionAddress; +import javax.media.rtp.rtcp.SourceDescription; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; + +/** + * An Easy to use Audio Channel implemented using JMF. + * It sends and receives jmf for and from desired IPs and ports. + * Also has a rport Symetric behavior for better NAT Traversal. + * It send data from a defined port and receive data in the same port, making NAT binds easier. + *

+ * Send from portA to portB and receive from portB in portA. + *

+ * Sending + * portA ---> portB + *

+ * Receiving + * portB ---> portA + *

+ * Transmit and Receive are interdependents. To receive you MUST trasmit. + */ +public class AudioChannel { + + private MediaLocator locator; + private String localIpAddress; + private String ipAddress; + private int localPort; + private int portBase; + private Format format; + + private Processor processor = null; + private RTPManager rtpMgrs[]; + private DataSource dataOutput = null; + private AudioReceiver audioReceiver; + + private List sendStreams = new ArrayList(); + + private boolean started = false; + + /** + * Creates an Audio Channel for a desired jmf locator. For instance: new MediaLocator("dsound://") + * + * @param locator + * @param ipAddress + * @param localPort + * @param remotePort + * @param format + */ + public AudioChannel(MediaLocator locator, + String localIpAddress, + String ipAddress, + int localPort, + int remotePort, + Format format) { + + this.locator = locator; + this.localIpAddress = localIpAddress; + this.ipAddress = ipAddress; + this.localPort = localPort; + this.portBase = remotePort; + this.format = format; + } + + /** + * Starts the transmission. Returns null if transmission started ok. + * Otherwise it returns a string with the reason why the setup failed. + * Starts receive also. + */ + public synchronized String start() { + if (started) return null; + started = true; + String result; + + // Create a processor for the specified jmf locator + result = createProcessor(); + if (result != null) { + started = false; + return result; + } + + // Create an RTP session to transmit the output of the + // processor to the specified IP address and port no. + result = createTransmitter(); + if (result != null) { + processor.close(); + processor = null; + started = false; + return result; + } + + // Start the transmission + processor.start(); + + return null; + } + + /** + * Stops the transmission if already started. + * Stops the receiver also. + */ + public void stop() { + if (!started) return; + synchronized (this) { + try { + started = false; + if (processor != null) { + processor.stop(); + processor = null; + + for (int i = 0; i < rtpMgrs.length; i++) { + rtpMgrs[i].removeReceiveStreamListener(audioReceiver); + rtpMgrs[i].removeSessionListener(audioReceiver); + rtpMgrs[i].removeTargets("Session ended."); + rtpMgrs[i].dispose(); + } + + sendStreams.clear(); + + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private String createProcessor() { + if (locator == null) + return "Locator is null"; + + DataSource ds; + + try { + ds = javax.media.Manager.createDataSource(locator); + } catch (Exception e) { + e.printStackTrace(); + return "Couldn't create DataSource"; + } + + // Try to create a processor to handle the input jmf locator + try { + processor = javax.media.Manager.createProcessor(ds); + } catch (NoProcessorException npe) { + npe.printStackTrace(); + return "Couldn't create processor"; + } catch (IOException ioe) { + ioe.printStackTrace(); + return "IOException creating processor"; + } + + // Wait for it to configure + boolean result = waitForState(processor, Processor.Configured); + if (result == false) + return "Couldn't configure processor"; + + // Get the tracks from the processor + TrackControl[] tracks = processor.getTrackControls(); + + // Do we have atleast one track? + if (tracks == null || tracks.length < 1) + return "Couldn't find tracks in processor"; + + // Set the output content descriptor to RAW_RTP + // This will limit the supported formats reported from + // Track.getSupportedFormats to only valid RTP formats. + ContentDescriptor cd = new ContentDescriptor(ContentDescriptor.RAW_RTP); + processor.setContentDescriptor(cd); + + Format supported[]; + Format chosen = null; + boolean atLeastOneTrack = false; + + // Program the tracks. + for (int i = 0; i < tracks.length; i++) { + if (tracks[i].isEnabled()) { + + supported = tracks[i].getSupportedFormats(); + + if (supported.length > 0) { + for (Format format : supported) { + if (format instanceof AudioFormat) { + if (this.format.matches(format)) + chosen = format; + } + } + if (chosen != null) { + tracks[i].setFormat(chosen); + System.err.println("Track " + i + " is set to transmit as:"); + System.err.println(" " + chosen); + atLeastOneTrack = true; + } else + tracks[i].setEnabled(false); + } else + tracks[i].setEnabled(false); + } + } + + if (!atLeastOneTrack) + return "Couldn't set any of the tracks to a valid RTP format"; + + result = waitForState(processor, Controller.Realized); + if (result == false) + return "Couldn't realize processor"; + + // Get the output data source of the processor + dataOutput = processor.getDataOutput(); + + return null; + } + + + /** + * Use the RTPManager API to create sessions for each jmf + * track of the processor. + */ + private String createTransmitter() { + + // Cheated. Should have checked the type. + PushBufferDataSource pbds = (PushBufferDataSource) dataOutput; + PushBufferStream pbss[] = pbds.getStreams(); + + rtpMgrs = new RTPManager[pbss.length]; + SessionAddress localAddr, destAddr; + InetAddress ipAddr; + SendStream sendStream; + audioReceiver = new AudioReceiver(this); + int port; + SourceDescription srcDesList[]; + + for (int i = 0; i < pbss.length; i++) { + try { + rtpMgrs[i] = RTPManager.newInstance(); + + port = portBase + 2 * i; + ipAddr = InetAddress.getByName(ipAddress); + + localAddr = new SessionAddress(InetAddress.getByName(this.localIpAddress), + localPort); + + destAddr = new SessionAddress(ipAddr, port); + + rtpMgrs[i].addReceiveStreamListener(audioReceiver); + rtpMgrs[i].addSessionListener(audioReceiver); + + rtpMgrs[i].initialize(localAddr); + + rtpMgrs[i].addTarget(destAddr); + + System.err.println("Created RTP session at " + localPort + " to: " + ipAddress + " " + port); + + sendStream = rtpMgrs[i].createSendStream(dataOutput, i); + + sendStreams.add(sendStream); + + sendStream.start(); + + } catch (Exception e) { + e.printStackTrace(); + return e.getMessage(); + } + } + + return null; + } + + /** + * Set transmit activity. If the active is true, the instance should trasmit. + * If it is set to false, the instance should pause transmit. + * + * @param active + */ + public void setTrasmit(boolean active) { + for (SendStream sendStream : sendStreams) { + try { + if (active) { + sendStream.start(); + System.out.println("START"); + } else { + sendStream.stop(); + System.out.println("STOP"); + } + } + catch (IOException e) { + e.printStackTrace(); + } + + } + } + + /** + * ************************************************************* + * Convenience methods to handle processor's state changes. + * ************************************************************** + */ + + private Integer stateLock = new Integer(0); + private boolean failed = false; + + Integer getStateLock() { + return stateLock; + } + + void setFailed() { + failed = true; + } + + private synchronized boolean waitForState(Processor p, int state) { + p.addControllerListener(new StateListener()); + failed = false; + + // Call the required method on the processor + if (state == Processor.Configured) { + p.configure(); + } else if (state == Processor.Realized) { + p.realize(); + } + + // Wait until we get an event that confirms the + // success of the method, or a failure event. + // See StateListener inner class + while (p.getState() < state && !failed) { + synchronized (getStateLock()) { + try { + getStateLock().wait(); + } catch (InterruptedException ie) { + return false; + } + } + } + + if (failed) + return false; + else + return true; + } + + /** + * ************************************************************* + * Inner Classes + * ************************************************************** + */ + + class StateListener implements ControllerListener { + + public void controllerUpdate(ControllerEvent ce) { + + // If there was an error during configure or + // realize, the processor will be closed + if (ce instanceof ControllerClosedEvent) + setFailed(); + + // All controller events, send a notification + // to the waiting thread in waitForState method. + if (ce instanceof ControllerEvent) { + synchronized (getStateLock()) { + getStateLock().notifyAll(); + } + } + } + } + + public static void main(String args[]) { + + InetAddress localhost; + try { + localhost = InetAddress.getLocalHost(); + + AudioChannel audioChannel0 = new AudioChannel(new MediaLocator("javasound://8000"), localhost.getHostAddress(), localhost.getHostAddress(), 7002, 7020, new AudioFormat(AudioFormat.GSM_RTP)); + AudioChannel audioChannel1 = new AudioChannel(new MediaLocator("javasound://8000"), localhost.getHostAddress(), localhost.getHostAddress(), 7020, 7002, new AudioFormat(AudioFormat.GSM_RTP)); + + audioChannel0.start(); + audioChannel1.start(); + + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + audioChannel0.setTrasmit(false); + audioChannel1.setTrasmit(false); + + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + audioChannel0.setTrasmit(true); + audioChannel1.setTrasmit(true); + + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + audioChannel0.stop(); + audioChannel1.stop(); + + } catch (UnknownHostException e) { + e.printStackTrace(); + } + + + } +} \ No newline at end of file diff --git a/jingle/media/source/org/jivesoftware/jingleaudio/jmf/AudioFormatUtils.java b/jingle/media/source/org/jivesoftware/jingleaudio/jmf/AudioFormatUtils.java new file mode 100644 index 000000000..70eb5db89 --- /dev/null +++ b/jingle/media/source/org/jivesoftware/jingleaudio/jmf/AudioFormatUtils.java @@ -0,0 +1,51 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: 08/11/2006 + *

+ * Copyright 2003-2006 Jive Software. + *

+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.jingleaudio.jmf; + +import org.jivesoftware.smackx.jingle.media.PayloadType; + +import javax.media.format.AudioFormat; + +/** + * Audio Format Ttils. + */ +public class AudioFormatUtils { + + /** + * Return a JMF AudioFormat for a given Jingle Payload type. + * Return null if the payload is not supported by this jmf API. + * + * @param payloadtype + * @return + */ + public static AudioFormat getAudioFormat(PayloadType payloadtype) { + + switch (payloadtype.getId()) { + case 3: + return new AudioFormat(AudioFormat.GSM_RTP); + case 4: + return new AudioFormat(AudioFormat.G723_RTP); + default: + return null; + } + + } + +} diff --git a/jingle/media/source/org/jivesoftware/jingleaudio/jmf/AudioMediaSession.java b/jingle/media/source/org/jivesoftware/jingleaudio/jmf/AudioMediaSession.java new file mode 100644 index 000000000..d85a21168 --- /dev/null +++ b/jingle/media/source/org/jivesoftware/jingleaudio/jmf/AudioMediaSession.java @@ -0,0 +1,155 @@ +package org.jivesoftware.jingleaudio.jmf; /** + * $RCSfile$ + * $Revision: $ + * $Date: 08/11/2006 + *

+ * Copyright 2003-2006 Jive Software. + *

+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.jivesoftware.jingleaudio.jmf.AudioChannel; +import org.jivesoftware.jingleaudio.jmf.AudioFormatUtils; +import org.jivesoftware.smackx.jingle.media.JingleMediaSession; +import org.jivesoftware.smackx.jingle.media.PayloadType; +import org.jivesoftware.smackx.jingle.nat.TransportCandidate; + +import javax.media.MediaLocator; +import javax.media.format.AudioFormat; +import java.io.IOException; +import java.net.ServerSocket; + +/** + * This Class implements a complete JingleMediaSession. + * It sould be used to transmit and receive audio captured from the Mic. + * This Class should be automaticly controlled by JingleSession. + * But you could also use in any VOIP application. + * For better NAT Traversal support this implementation donīt support only receive or only transmit. + * To receive you MUST transmit. So the only implemented and functionally methods are startTransmit() and stopTransmit() + */ +public class AudioMediaSession extends JingleMediaSession { + + private AudioFormat format; + private AudioChannel audioChannel; + + /** + * Creates a org.jivesoftware.jingleaudio.jmf.AudioMediaSession with defined payload type, remote and local candidates + * + * @param payloadType Payload of the jmf + * @param remote The remote information. The candidate that the jmf will be sent to. + * @param local The local information. The candidate that will receive the jmf + */ + public AudioMediaSession(final PayloadType payloadType, final TransportCandidate remote, + final TransportCandidate local) { + super(payloadType, remote, local); + } + + /** + * Initialize the Audio Channel to make it able to send and receive audio + */ + public void initialize() { + + String ip; + String localIp; + int localPort; + int remotePort; + + if (this.getLocal().getSymmetric() != null) { + ip = this.getLocal().getIp(); + localIp = this.getLocal().getLocalIp(); + localPort = getFreePort(); + remotePort = this.getLocal().getSymmetric().getPort(); + + System.out.println(this.getLocal().getConnection() + " " + ip + ": " + localPort + "->" + remotePort); + + } else { + ip = this.getRemote().getIp(); + localIp = this.getLocal().getLocalIp(); + localPort = this.getLocal().getPort(); + remotePort = this.getRemote().getPort(); + } + + audioChannel = new AudioChannel(new MediaLocator("dsound://"), localIp, ip, localPort, remotePort, AudioFormatUtils.getAudioFormat(this.getPayloadType())); + } + + /** + * Starts transmission and for NAT Traversal reasons start receiving also. + */ + public void startTrasmit() { + audioChannel.start(); + } + + /** + * Set transmit activity. If the active is true, the instance should trasmit. + * If it is set to false, the instance should pause transmit. + * + * @param active + */ + public void setTrasmit(boolean active) { + audioChannel.setTrasmit(active); + } + + /** + * For NAT Reasons this method does nothing. Use startTransmit() to start transmit and receive jmf + */ + public void startReceive() { + // Do nothing + } + + /** + * Stops transmission and for NAT Traversal reasons stop receiving also. + */ + public void stopTrasmit() { + audioChannel.stop(); + } + + /** + * For NAT Reasons this method does nothing. Use startTransmit() to start transmit and receive jmf + */ + public void stopReceive() { + // Do nothing + } + + /** + * Obtain a free port we can use. + * + * @return A free port number. + */ + protected int getFreePort() { + ServerSocket ss; + int freePort = 0; + + for (int i = 0; i < 10; i++) { + freePort = (int) (10000 + Math.round(Math.random() * 10000)); + freePort = freePort % 2 == 0 ? freePort : freePort + 1; + try { + ss = new ServerSocket(freePort); + freePort = ss.getLocalPort(); + ss.close(); + return freePort; + } + catch (IOException e) { + e.printStackTrace(); + } + } + try { + ss = new ServerSocket(0); + freePort = ss.getLocalPort(); + ss.close(); + } + catch (IOException e) { + e.printStackTrace(); + } + return freePort; + } +} diff --git a/jingle/media/source/org/jivesoftware/jingleaudio/jmf/AudioReceiver.java b/jingle/media/source/org/jivesoftware/jingleaudio/jmf/AudioReceiver.java new file mode 100644 index 000000000..f7645bd8d --- /dev/null +++ b/jingle/media/source/org/jivesoftware/jingleaudio/jmf/AudioReceiver.java @@ -0,0 +1,143 @@ +package org.jivesoftware.jingleaudio.jmf; /** + * $RCSfile$ + * $Revision: $ + * $Date: 08/11/2006 + *

+ * Copyright 2003-2006 Jive Software. + *

+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import javax.media.*; +import javax.media.protocol.DataSource; +import javax.media.rtp.*; +import javax.media.rtp.event.*; + +/** + * This class implements receive methods and listeners to be used in AudioChannel + */ +public class AudioReceiver implements ReceiveStreamListener, SessionListener, + ControllerListener { + + boolean dataReceived = false; + + Object dataSync; + + public AudioReceiver(Object dataSync) { + this.dataSync = dataSync; + } + + /** + * JingleSessionListener. + */ + public synchronized void update(SessionEvent evt) { + if (evt instanceof NewParticipantEvent) { + Participant p = ((NewParticipantEvent) evt).getParticipant(); + System.err.println(" - A new participant had just joined: " + p.getCNAME()); + } + } + + /** + * ReceiveStreamListener + */ + public synchronized void update(ReceiveStreamEvent evt) { + + RTPManager mgr = (RTPManager) evt.getSource(); + Participant participant = evt.getParticipant(); // could be null. + ReceiveStream stream = evt.getReceiveStream(); // could be null. + + if (evt instanceof RemotePayloadChangeEvent) { + + System.err.println(" - Received an RTP PayloadChangeEvent."); + System.err.println("Sorry, cannot handle payload change."); + // System.exit(0); + + } else if (evt instanceof NewReceiveStreamEvent) { + + try { + stream = ((NewReceiveStreamEvent) evt).getReceiveStream(); + DataSource ds = stream.getDataSource(); + + // Find out the formats. + RTPControl ctl = (RTPControl) ds.getControl("javax.jmf.rtp.RTPControl"); + if (ctl != null) { + System.err.println(" - Recevied new RTP stream: " + ctl.getFormat()); + } else + System.err.println(" - Recevied new RTP stream"); + + if (participant == null) + System.err.println(" The sender of this stream had yet to be identified."); + else { + System.err.println(" The stream comes from: " + participant.getCNAME()); + } + + // create a player by passing datasource to the Media Manager + Player p = javax.media.Manager.createPlayer(ds); + if (p == null) + return; + + p.addControllerListener(this); + p.realize(); + + // Notify intialize() that a new stream had arrived. + synchronized (dataSync) { + dataReceived = true; + dataSync.notifyAll(); + } + + } catch (Exception e) { + System.err.println("NewReceiveStreamEvent exception " + e.getMessage()); + return; + } + + } else if (evt instanceof StreamMappedEvent) { + + if (stream != null && stream.getDataSource() != null) { + DataSource ds = stream.getDataSource(); + // Find out the formats. + RTPControl ctl = (RTPControl) ds.getControl("javax.jmf.rtp.RTPControl"); + System.err.println(" - The previously unidentified stream "); + if (ctl != null) + System.err.println(" " + ctl.getFormat()); + System.err.println(" had now been identified as sent by: " + participant.getCNAME()); + } + } else if (evt instanceof ByeEvent) { + + System.err.println(" - Got \"bye\" from: " + participant.getCNAME()); + + } + + } + + /** + * ControllerListener for the Players. + */ + public synchronized void controllerUpdate(ControllerEvent ce) { + + Player p = (Player) ce.getSourceController(); + + if (p == null) + return; + + // Get this when the internal players are realized. + if (ce instanceof RealizeCompleteEvent) { + p.start(); + } + + if (ce instanceof ControllerErrorEvent) { + p.removeControllerListener(this); + System.err.println("Receiver internal error: " + ce); + } + + } +} diff --git a/jingle/media/source/org/jivesoftware/jingleaudio/jmf/JmfMediaManager.java b/jingle/media/source/org/jivesoftware/jingleaudio/jmf/JmfMediaManager.java new file mode 100644 index 000000000..a6f0e3dd1 --- /dev/null +++ b/jingle/media/source/org/jivesoftware/jingleaudio/jmf/JmfMediaManager.java @@ -0,0 +1,119 @@ +package org.jivesoftware.jingleaudio.jmf; + +/** + * $RCSfile$ + * $Revision: $ + * $Date: 08/11/2006 + *

+ * Copyright 2003-2006 Jive Software. + *

+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.jivesoftware.smackx.jingle.media.JingleMediaManager; +import org.jivesoftware.smackx.jingle.media.JingleMediaSession; +import org.jivesoftware.smackx.jingle.media.PayloadType; +import org.jivesoftware.smackx.jingle.nat.TransportCandidate; + +import java.io.File; +import java.io.IOException; + +/** + * Implements a jingleMediaManager using JMF based API. + * It supports GSM and G723 codecs. + * This API only currently works on windows. + */ +public class JmfMediaManager extends JingleMediaManager { + + /** + * Creates a Media Manager instance + */ + public JmfMediaManager() { + setupPayloads(); + setupJMF(); + } + + /** + * Returns a new jingleMediaSession + * + * @param payloadType + * @param remote + * @param local + * @return + */ + public JingleMediaSession createMediaSession(final PayloadType payloadType, final TransportCandidate remote, final TransportCandidate local) { + return new AudioMediaSession(payloadType, remote, local); + } + + /** + * Setup API supported Payloads + */ + private void setupPayloads() { + this.addPayloadType(new PayloadType.Audio(3, "gsm")); + this.addPayloadType(new PayloadType.Audio(4, "g723")); + } + + /** + * Runs JMFInit the first time the application is started so that capture + * devices are properly detected and initialized by JMF. + */ + public static void setupJMF() { + try { + + // .jmf is the place where we store the jmf.properties file used + // by JMF. if the directory does not exist or it does not contain + // a jmf.properties file. or if the jmf.properties file has 0 length + // then this is the first time we're running and should continue to + // with JMFInit + String homeDir = System.getProperty("user.home"); + File jmfDir = new File(homeDir, ".jmf"); + String classpath = System.getProperty("java.class.path"); + classpath += System.getProperty("path.separator") + + jmfDir.getAbsolutePath(); + System.setProperty("java.class.path", classpath); + + if (!jmfDir.exists()) + jmfDir.mkdir(); + + File jmfProperties = new File(jmfDir, "jmf.properties"); + + if (!jmfProperties.exists()) { + try { + jmfProperties.createNewFile(); + } + catch (IOException ex) { + System.out.println("Failed to create jmf.properties"); + ex.printStackTrace(); + } + } + + // if we're running on linux checkout that libjmutil.so is where it + // should be and put it there. + runLinuxPreInstall(); + + if (jmfProperties.length() == 0) { + //JMFInit init = new JMFInit(null); + //init.setVisible(false); + } + + } + finally { + + } + + } + + private static void runLinuxPreInstall() { + // @TODO Implement Linux Pre-Install + } +} diff --git a/jingle/media/source/org/jivesoftware/jingleaudio/jspeex/AudioMediaSession.java b/jingle/media/source/org/jivesoftware/jingleaudio/jspeex/AudioMediaSession.java new file mode 100644 index 000000000..897beb65c --- /dev/null +++ b/jingle/media/source/org/jivesoftware/jingleaudio/jspeex/AudioMediaSession.java @@ -0,0 +1,209 @@ +package org.jivesoftware.jingleaudio.jspeex; + +import mil.jfcom.cie.media.session.MediaSession; +import mil.jfcom.cie.media.session.MediaSessionListener; +import mil.jfcom.cie.media.session.StreamPlayer; +import mil.jfcom.cie.media.srtp.packetizer.SpeexFormat; +import org.jivesoftware.smackx.jingle.media.JingleMediaSession; +import org.jivesoftware.smackx.jingle.media.PayloadType; +import org.jivesoftware.smackx.jingle.nat.TransportCandidate; + +import javax.media.NoProcessorException; +import javax.media.format.UnsupportedFormatException; +import javax.media.rtp.rtcp.SenderReport; +import javax.media.rtp.rtcp.SourceDescription; +import java.io.IOException; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.security.GeneralSecurityException; + +/** + * $RCSfile$ + * $Revision: $ + * $Date: 25/12/2006 + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +public class AudioMediaSession extends JingleMediaSession implements MediaSessionListener { + + private MediaSession mediaSession; + + /** + * Create session for test program. + * + * @param micOn microphone turned on + */ + public static MediaSession createSession(String localhost, int localPort, String remoteHost, int remotePort, MediaSessionListener eventHandler, int quality, boolean secure, boolean micOn) throws NoProcessorException, UnsupportedFormatException, IOException, GeneralSecurityException { + + SpeexFormat.setFramesPerPacket(1); + /** + * The master key. Hardcoded for now. + */ + byte[] masterKey = new byte[]{(byte) 0xE1, (byte) 0xF9, 0x7A, 0x0D, 0x3E, 0x01, (byte) 0x8B, (byte) 0xE0, (byte) 0xD6, 0x4F, (byte) 0xA3, 0x2C, 0x06, (byte) 0xDE, 0x41, 0x39}; + + /** + * The master salt. Hardcoded for now. + */ + byte[] masterSalt = new byte[]{0x0E, (byte) 0xC6, 0x75, (byte) 0xAD, 0x49, (byte) 0x8A, (byte) 0xFE, (byte) 0xEB, (byte) 0xB6, (byte) 0x96, 0x0B, 0x3A, (byte) 0xAB, (byte) 0xE6}; + + DatagramSocket[] localPorts = MediaSession.getLocalPorts(InetAddress.getByName(localhost), localPort); + MediaSession session = MediaSession.createInstance(remoteHost, remotePort, localPorts, quality, secure, masterKey, masterSalt); + session.setListener(eventHandler); + + session.setSourceDescription(new SourceDescription[]{new SourceDescription(SourceDescription.SOURCE_DESC_NAME, "Superman", 1, false), new SourceDescription(SourceDescription.SOURCE_DESC_EMAIL, "cdcie.tester@je.jfcom.mil", 1, false), new SourceDescription(SourceDescription.SOURCE_DESC_LOC, InetAddress.getByName(localhost) + " Port " + session.getLocalDataPort(), 1, false), new SourceDescription(SourceDescription.SOURCE_DESC_TOOL, "JFCOM CDCIE Audio Chat", 1, false)}); + return session; + } + + + /** + * Creates a org.jivesoftware.jingleaudio.jmf.AudioMediaSession with defined payload type, remote and local candidates + * + * @param payloadType Payload of the jmf + * @param remote The remote information. The candidate that the jmf will be sent to. + * @param local The local information. The candidate that will receive the jmf + */ + public AudioMediaSession(final PayloadType payloadType, final TransportCandidate remote, + final TransportCandidate local) { + super(payloadType, remote, local); + } + + /** + * Initialize the Audio Channel to make it able to send and receive audio + */ + public void initialize() { + + String ip; + String localIp; + int localPort; + int remotePort; + + if (this.getLocal().getSymmetric() != null) { + ip = this.getLocal().getIp(); + localIp = this.getLocal().getLocalIp(); + localPort = getFreePort(); + remotePort = this.getLocal().getSymmetric().getPort(); + + System.out.println(this.getLocal().getConnection() + " " + ip + ": " + localPort + "->" + remotePort); + + } else { + ip = this.getRemote().getIp(); + localIp = this.getLocal().getLocalIp(); + localPort = this.getLocal().getPort(); + remotePort = this.getRemote().getPort(); + } + + try { + mediaSession = createSession(localIp, localPort, ip, remotePort, this, 2, false, true); + } catch (NoProcessorException e) { + e.printStackTrace(); + } catch (UnsupportedFormatException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (GeneralSecurityException e) { + e.printStackTrace(); + } + } + + /** + * Starts transmission and for NAT Traversal reasons start receiving also. + */ + public void startTrasmit() { + try { + System.out.println("start"); + mediaSession.start(true); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Set transmit activity. If the active is true, the instance should trasmit. + * If it is set to false, the instance should pause transmit. + * + * @param active + */ + public void setTrasmit(boolean active) { + // Do nothing + } + + /** + * For NAT Reasons this method does nothing. Use startTransmit() to start transmit and receive jmf + */ + public void startReceive() { + // Do nothing + } + + /** + * Stops transmission and for NAT Traversal reasons stop receiving also. + */ + public void stopTrasmit() { + // Do nothing + } + + /** + * For NAT Reasons this method does nothing. Use startTransmit() to start transmit and receive jmf + */ + public void stopReceive() { + // Do nothing + } + + public void newStreamIdentified(StreamPlayer streamPlayer) { + //To change body of implemented methods use File | Settings | File Templates. + } + + public void senderReportReceived(SenderReport report) { + //To change body of implemented methods use File | Settings | File Templates. + } + + public void streamClosed(StreamPlayer stream, boolean timeout) { + //To change body of implemented methods use File | Settings | File Templates. + } + + /** + * Obtain a free port we can use. + * + * @return A free port number. + */ + protected int getFreePort() { + ServerSocket ss; + int freePort = 0; + + for (int i = 0; i < 10; i++) { + freePort = (int) (10000 + Math.round(Math.random() * 10000)); + freePort = freePort % 2 == 0 ? freePort : freePort + 1; + try { + ss = new ServerSocket(freePort); + freePort = ss.getLocalPort(); + ss.close(); + return freePort; + } + catch (IOException e) { + e.printStackTrace(); + } + } + try { + ss = new ServerSocket(0); + freePort = ss.getLocalPort(); + ss.close(); + } + catch (IOException e) { + e.printStackTrace(); + } + return freePort; + } +} diff --git a/jingle/media/source/org/jivesoftware/jingleaudio/jspeex/SpeexMediaManager.java b/jingle/media/source/org/jivesoftware/jingleaudio/jspeex/SpeexMediaManager.java new file mode 100644 index 000000000..6b8a61ecd --- /dev/null +++ b/jingle/media/source/org/jivesoftware/jingleaudio/jspeex/SpeexMediaManager.java @@ -0,0 +1,101 @@ +package org.jivesoftware.jingleaudio.jspeex; + +import org.jivesoftware.smackx.jingle.media.JingleMediaManager; +import org.jivesoftware.smackx.jingle.media.JingleMediaSession; +import org.jivesoftware.smackx.jingle.media.PayloadType; +import org.jivesoftware.smackx.jingle.nat.TransportCandidate; + +import java.io.File; +import java.io.IOException; + +/** + * $RCSfile$ + * $Revision: $ + * $Date: 25/12/2006 + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +public class SpeexMediaManager extends JingleMediaManager { + + public SpeexMediaManager() { + setupPayloads(); + setupJMF(); + } + + public JingleMediaSession createMediaSession(PayloadType payloadType, final TransportCandidate remote, final TransportCandidate local) { + return new AudioMediaSession(payloadType, remote, local); + } + + /** + * Setup API supported Payloads + */ + private void setupPayloads() { + this.addPayloadType(new PayloadType.Audio(15, "speex")); + } + + /** + * Runs JMFInit the first time the application is started so that capture + * devices are properly detected and initialized by JMF. + */ + public static void setupJMF() { + try { + + // .jmf is the place where we store the jmf.properties file used + // by JMF. if the directory does not exist or it does not contain + // a jmf.properties file. or if the jmf.properties file has 0 length + // then this is the first time we're running and should continue to + // with JMFInit + String homeDir = System.getProperty("user.home"); + File jmfDir = new File(homeDir, ".jmf"); + String classpath = System.getProperty("java.class.path"); + classpath += System.getProperty("path.separator") + + jmfDir.getAbsolutePath(); + System.setProperty("java.class.path", classpath); + + if (!jmfDir.exists()) + jmfDir.mkdir(); + + File jmfProperties = new File(jmfDir, "jmf.properties"); + + if (!jmfProperties.exists()) { + try { + jmfProperties.createNewFile(); + } + catch (IOException ex) { + System.out.println("Failed to create jmf.properties"); + ex.printStackTrace(); + } + } + + // if we're running on linux checkout that libjmutil.so is where it + // should be and put it there. + runLinuxPreInstall(); + + if (jmfProperties.length() == 0) { + //JMFInit init = new JMFInit(null); + //init.setVisible(false); + } + + } + finally { + + } + + } + + private static void runLinuxPreInstall() { + // @TODO Implement Linux Pre-Install + } +} diff --git a/jingle/media/test/JingleMediaTest.java b/jingle/media/test/JingleMediaTest.java new file mode 100644 index 000000000..ea3ccaec6 --- /dev/null +++ b/jingle/media/test/JingleMediaTest.java @@ -0,0 +1,362 @@ +import junit.framework.TestCase; +import org.jivesoftware.jingleaudio.jmf.AudioChannel; +import org.jivesoftware.jingleaudio.jmf.JmfMediaManager; +import org.jivesoftware.jingleaudio.jspeex.SpeexMediaManager; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.jingle.IncomingJingleSession; +import org.jivesoftware.smackx.jingle.JingleManager; +import org.jivesoftware.smackx.jingle.JingleSessionRequest; +import org.jivesoftware.smackx.jingle.OutgoingJingleSession; +import org.jivesoftware.smackx.jingle.listeners.JingleSessionRequestListener; +import org.jivesoftware.smackx.jingle.media.JingleMediaManager; +import org.jivesoftware.smackx.jingle.nat.BridgedTransportManager; +import org.jivesoftware.smackx.jingle.nat.ICETransportManager; +import org.jivesoftware.smackx.jingle.nat.STUNTransportManager; + +import javax.media.MediaLocator; +import javax.media.format.AudioFormat; +import java.net.InetAddress; + +/** + * $RCSfile$ + * $Revision: $ + * $Date: 09/11/2006 + *

+ * Copyright 2003-2006 Jive Software. + *

+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +public class JingleMediaTest extends TestCase { + + public void testCompleteJmf() { + + try { + + //XMPPConnection.DEBUG_ENABLED = true; + + XMPPConnection x0 = new XMPPConnection("thiago"); + XMPPConnection x1 = new XMPPConnection("thiago"); + + x0.connect(); + x0.login("barata7", "barata7"); + x1.connect(); + x1.login("barata6", "barata6"); + + final JingleManager jm0 = new JingleManager( + x0, new ICETransportManager()); + final JingleManager jm1 = new JingleManager( + x1, new ICETransportManager()); + + JingleMediaManager jingleMediaManager0 = new JmfMediaManager(); + JingleMediaManager jingleMediaManager1 = new JmfMediaManager(); + + jm0.setMediaManager(jingleMediaManager0); + jm1.setMediaManager(jingleMediaManager1); + + jm1.addJingleSessionRequestListener(new JingleSessionRequestListener() { + public void sessionRequested(final JingleSessionRequest request) { + + try { + IncomingJingleSession session = request.accept(jm1.getMediaManager().getPayloads()); + session.start(request); + } catch (XMPPException e) { + e.printStackTrace(); + } + + } + }); + + OutgoingJingleSession js0 = jm0.createOutgoingJingleSession("barata6@thiago/Smack"); + + js0.start(); + + Thread.sleep(50000); + js0.terminate(); + + Thread.sleep(6000); + + x0.disconnect(); + x1.disconnect(); + + } catch (Exception e) { + e.printStackTrace(); + } + + } + + public void testCompleteSpeex() { + + try { + + //XMPPConnection.DEBUG_ENABLED = true; + + XMPPConnection x0 = new XMPPConnection("thiago"); + XMPPConnection x1 = new XMPPConnection("thiago"); + + x0.connect(); + x0.login("barata7", "barata7"); + x1.connect(); + x1.login("barata6", "barata6"); + + final JingleManager jm0 = new JingleManager( + x0, new STUNTransportManager()); + final JingleManager jm1 = new JingleManager( + x1, new STUNTransportManager()); + + JingleMediaManager jingleMediaManager0 = new SpeexMediaManager(); + JingleMediaManager jingleMediaManager1 = new SpeexMediaManager(); + + jm0.setMediaManager(jingleMediaManager0); + jm1.setMediaManager(jingleMediaManager1); + + jm1.addJingleSessionRequestListener(new JingleSessionRequestListener() { + public void sessionRequested(final JingleSessionRequest request) { + + try { + + IncomingJingleSession session = request.accept(jm1.getMediaManager().getPayloads()); + + session.start(request); + } catch (XMPPException e) { + e.printStackTrace(); + } + + } + }); + + OutgoingJingleSession js0 = jm0.createOutgoingJingleSession("barata6@thiago/Smack"); + + js0.start(); + + Thread.sleep(150000); + js0.terminate(); + + Thread.sleep(6000); + + x0.disconnect(); + x1.disconnect(); + + } catch (Exception e) { + e.printStackTrace(); + } + + } + + public void testCompleteWithBridge() { + + for (int i = 0; i < 1; i += 2) { + final int n = i; + Thread t = new Thread(new Runnable() { + public void run() { + try { + + XMPPConnection x0 = new XMPPConnection("thiago"); + XMPPConnection x1 = new XMPPConnection("thiago"); + + x0.connect(); + x0.login("user" + String.valueOf(n), "user" + String.valueOf(n)); + x1.connect(); + x1.login("user" + String.valueOf(n + 1), "user" + String.valueOf(n + 1)); + + BridgedTransportManager btm0 = new BridgedTransportManager(x0); + BridgedTransportManager btm1 = new BridgedTransportManager(x1); + + final JingleManager jm0 = new JingleManager(x0, btm0); + final JingleManager jm1 = new JingleManager(x1, btm1); + + jm0.addCreationListener(btm0); + jm1.addCreationListener(btm1); + + JingleMediaManager jingleMediaManager = new SpeexMediaManager(); + JingleMediaManager jingleMediaManager2 = new SpeexMediaManager(); + + jm0.setMediaManager(jingleMediaManager); + jm1.setMediaManager(jingleMediaManager2); + + jm1.addJingleSessionRequestListener(new JingleSessionRequestListener() { + public void sessionRequested(final JingleSessionRequest request) { + + try { + IncomingJingleSession session = request.accept(jm1.getMediaManager().getPayloads()); + + session.start(request); + } catch (XMPPException e) { + e.printStackTrace(); + } + + } + }); + + OutgoingJingleSession js0 = jm0.createOutgoingJingleSession("user" + String.valueOf(n + 1) + "@thiago/Smack"); + + js0.start(); + + Thread.sleep(55000); + + js0.terminate(); + + Thread.sleep(3000); + + x0.disconnect(); + x1.disconnect(); + + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + + t.start(); + } + + try { + Thread.sleep(250000); + } catch (InterruptedException e) { + e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. + } + } + + public void testCompleteWithBridgeB() { + try { + + //XMPPConnection.DEBUG_ENABLED = true; + + XMPPConnection x0 = new XMPPConnection("thiago"); + XMPPConnection x1 = new XMPPConnection("thiago"); + + x0.connect(); + x0.login("barata5", "barata5"); + x1.connect(); + x1.login("barata4", "barata4"); + + BridgedTransportManager btm0 = new BridgedTransportManager(x0); + BridgedTransportManager btm1 = new BridgedTransportManager(x1); + + final JingleManager jm0 = new JingleManager(x0, btm0); + final JingleManager jm1 = new JingleManager(x1, btm1); + + jm0.addCreationListener(btm0); + jm1.addCreationListener(btm1); + + JingleMediaManager jingleMediaManager = new JmfMediaManager(); + + jm0.setMediaManager(jingleMediaManager); + jm1.setMediaManager(jingleMediaManager); + + jm1.addJingleSessionRequestListener(new JingleSessionRequestListener() { + public void sessionRequested(final JingleSessionRequest request) { + + try { + + IncomingJingleSession session = request.accept(jm1.getMediaManager().getPayloads()); + + session.start(request); + } catch (XMPPException e) { + e.printStackTrace(); + } + + } + }); + + OutgoingJingleSession js0 = jm0.createOutgoingJingleSession("barata4@thiago/Smack"); + + js0.start(); + + Thread.sleep(20000); + + js0.terminate(); + + Thread.sleep(3000); + + js0 = jm0.createOutgoingJingleSession("barata4@thiago/Smack"); + + js0.start(); + + Thread.sleep(20000); + + js0.terminate(); + + Thread.sleep(3000); + + x0.disconnect(); + x1.disconnect(); + + } catch (Exception e) { + e.printStackTrace(); + } + + } + + public void testAudioChannelOpenClose + () { + for (int i = 0; i < 5; i++) { + try { + AudioChannel audioChannel0 = new AudioChannel(new MediaLocator("javasound://"), InetAddress.getLocalHost().getHostAddress(), InetAddress.getLocalHost().getHostAddress(), 7002, 7020, new AudioFormat(AudioFormat.GSM_RTP)); + AudioChannel audioChannel1 = new AudioChannel(new MediaLocator("javasound://"), InetAddress.getLocalHost().getHostAddress(), InetAddress.getLocalHost().getHostAddress(), 7020, 7002, new AudioFormat(AudioFormat.GSM_RTP)); + + audioChannel0.start(); + audioChannel1.start(); + + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + audioChannel0.stop(); + audioChannel1.stop(); + + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + public void testAudioChannelStartStop + () { + + try { + AudioChannel audioChannel0 = new AudioChannel(new MediaLocator("javasound://"), InetAddress.getLocalHost().getHostAddress(), InetAddress.getLocalHost().getHostAddress(), 7002, 7020, new AudioFormat(AudioFormat.GSM_RTP)); + AudioChannel audioChannel1 = new AudioChannel(new MediaLocator("javasound://"), InetAddress.getLocalHost().getHostAddress(), InetAddress.getLocalHost().getHostAddress(), 7020, 7002, new AudioFormat(AudioFormat.GSM_RTP)); + + for (int i = 0; i < 5; i++) { + + audioChannel0.start(); + audioChannel1.start(); + + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + audioChannel0.stop(); + audioChannel1.stop(); + + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } +} \ No newline at end of file