/** * $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.packet.Presence; import org.jivesoftware.smack.packet.PacketExtension; import org.jivesoftware.smack.provider.ProviderManager; import org.jivesoftware.smack.provider.PacketExtensionProvider; 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 org.jivesoftware.smackx.packet.JingleError; import org.xmlpull.v1.XmlPullParser; 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(Presence presence) { if (!presence.isAvailable()) { String xmppAddress = presence.getFrom(); 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(JingleSession session) { 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(JingleSession session) { 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:" + reason); } 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; if (jingleMediaManager != null) session = new OutgoingJingleSession(connection, responder, payloadTypes, jingleTransportManager, jingleMediaManager); else session = new OutgoingJingleSession(connection, responder, payloadTypes, jingleTransportManager); 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; if (jingleMediaManager != null) session = new IncomingJingleSession(connection, request .getFrom(), payloadTypes, jingleTransportManager, jingleMediaManager, request); else session = new IncomingJingleSession(connection, request .getFrom(), payloadTypes, jingleTransportManager, request); 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 createIncomingJingleSession(request, jingleMediaManager.getPayloads()); return createIncomingJingleSession(request, null); } /** * 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); } }