/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Copyright @ 2015 Atlassian Pty Ltd
*
* 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 net.java.sip.communicator.service.protocol.media;
import java.net.*;
import net.java.sip.communicator.service.netaddr.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.util.*;
import org.ice4j.ice.*;
import org.jitsi.service.configuration.*;
import org.jitsi.service.neomedia.*;
/**
* <tt>TransportManager</tt>s are responsible for allocating ports, gathering
* local candidates and managing ICE whenever we are using it.
*
* @param <U> the peer extension class like for example <tt>CallPeerSipImpl</tt>
* or <tt>CallPeerJabberImpl</tt>
*
* @author Emil Ivov
* @author Lyubomir Marinov
* @author Sebastien Vincent
*/
public abstract class TransportManager<U extends MediaAwareCallPeer<?, ?, ?>>
{
/**
* The <tt>Logger</tt> used by the <tt>TransportManager</tt>
* class and its instances for logging output.
*/
private static final Logger logger
= Logger.getLogger(TransportManager.class);
/**
* The port tracker that we should use when binding generic media streams.
* <p>
* Initialized by {@link #initializePortNumbers()}.
* </p>
*/
private static final PortTracker defaultPortTracker
= new PortTracker(5000, 6000);
/**
* The port tracker that we should use when binding video media streams.
* <p>
* Potentially initialized by {@link #initializePortNumbers()} if the
* necessary properties are set.
* </p>
*/
private static PortTracker videoPortTracker;
/**
* The port tracker that we should use when binding data channels.
* <p>
* Potentially initialized by {@link #initializePortNumbers()} if the
* necessary properties are set.
* </p>
*/
private static PortTracker dataPortTracker;
/**
* The port tracker that we should use when binding data media streams.
* <p>
* Potentially initialized by {@link #initializePortNumbers()} if the
* necessary properties are set.
* </p>
*/
private static PortTracker audioPortTracker;
/**
* RTP audio DSCP configuration property name.
*/
private static final String RTP_AUDIO_DSCP_PROPERTY =
"net.java.sip.communicator.impl.protocol.RTP_AUDIO_DSCP";
/**
* RTP video DSCP configuration property name.
*/
private static final String RTP_VIDEO_DSCP_PROPERTY =
"net.java.sip.communicator.impl.protocol.RTP_VIDEO_DSCP";
/**
* Number of empty UDP packets to send for NAT hole punching.
*/
private static final String HOLE_PUNCH_PKT_COUNT_PROPERTY =
"net.java.sip.communicator.impl.protocol.HOLE_PUNCH_PKT_COUNT";
/**
* Number of empty UDP packets to send for NAT hole punching.
*/
private static final int DEFAULT_HOLE_PUNCH_PKT_COUNT = 3;
/**
* Returns the port tracker that we are supposed to use when binding ports
* for the specified {@link MediaType}.
*
* @param mediaType the media type that we want to obtain the port tracker
* for. Use <tt>null</tt> to obtain the default port tracker.
*
* @return the port tracker that we are supposed to use when binding ports
* for the specified {@link MediaType}.
*/
protected static PortTracker getPortTracker(MediaType mediaType)
{
//make sure our port numbers reflect the configuration service settings
initializePortNumbers();
if (mediaType != null)
{
switch (mediaType)
{
case AUDIO:
if (audioPortTracker != null)
return audioPortTracker;
else
break;
case VIDEO:
if (videoPortTracker != null)
return videoPortTracker;
else
break;
case DATA:
if (dataPortTracker != null)
return dataPortTracker;
else
break;
}
}
return defaultPortTracker;
}
/**
* Returns the port tracker that we are supposed to use when binding ports
* for the {@link MediaType} indicated by the string param. If we do not
* recognize the string as a valid media type, we simply return the default
* port tracker.
*
* @param mediaTypeStr the name of the media type that we want to obtain a
* port tracker for.
*
* @return the port tracker that we are supposed to use when binding ports
* for the {@link MediaType} with the specified name or the default tracker
* in case the name doesn't ring a bell.
*/
protected static PortTracker getPortTracker(String mediaTypeStr)
{
try
{
return getPortTracker(MediaType.parseString(mediaTypeStr));
}
catch (Exception e)
{
logger.info(
"Returning default port tracker for unrecognized media type: "
+ mediaTypeStr);
return defaultPortTracker;
}
}
/**
* The {@link MediaAwareCallPeer} whose traffic we will be taking care of.
*/
private U callPeer;
/**
* The RTP/RTCP socket couples that this <tt>TransportManager</tt> uses to
* send and receive media flows through indexed by <tt>MediaType</tt>
* (ordinal).
*/
private final StreamConnector[] streamConnectors
= new StreamConnector[MediaType.values().length];
/**
* Creates a new instance of this transport manager, binding it to the
* specified peer.
*
* @param callPeer the {@link MediaAwareCallPeer} whose traffic we will be
* taking care of.
*/
protected TransportManager(U callPeer)
{
this.callPeer = callPeer;
}
/**
* Returns the <tt>StreamConnector</tt> instance that this media handler
* should use for streams of the specified <tt>mediaType</tt>. The method
* would also create a new <tt>StreamConnector</tt> if no connector has
* been initialized for this <tt>mediaType</tt> yet or in case one
* of its underlying sockets has been closed.
*
* @param mediaType the <tt>MediaType</tt> that we'd like to create a
* connector for.
* @return this media handler's <tt>StreamConnector</tt> for the specified
* <tt>mediaType</tt>.
*
* @throws OperationFailedException in case we failed to initialize our
* connector.
*/
public StreamConnector getStreamConnector(MediaType mediaType)
throws OperationFailedException
{
int streamConnectorIndex = mediaType.ordinal();
StreamConnector streamConnector
= streamConnectors[streamConnectorIndex];
if((streamConnector == null)
|| (streamConnector.getProtocol() == StreamConnector.Protocol.UDP))
{
DatagramSocket controlSocket;
if((streamConnector == null)
|| streamConnector.getDataSocket().isClosed()
|| (((controlSocket = streamConnector.getControlSocket())
!= null)
&& controlSocket.isClosed()))
{
streamConnectors[streamConnectorIndex]
= streamConnector
= createStreamConnector(mediaType);
}
}
else if(streamConnector.getProtocol() == StreamConnector.Protocol.TCP)
{
Socket controlTCPSocket;
if(streamConnector.getDataTCPSocket().isClosed()
|| (((controlTCPSocket = streamConnector.getControlTCPSocket())
!= null)
&& controlTCPSocket.isClosed()))
{
streamConnectors[streamConnectorIndex]
= streamConnector
= createStreamConnector(mediaType);
}
}
return streamConnector;
}
/**
* Closes the existing <tt>StreamConnector</tt>, if any, associated with a
* specific <tt>MediaType</tt> and removes its reference from this
* <tt>TransportManager</tt>.
*
* @param mediaType the <tt>MediaType</tt> associated with the
* <tt>StreamConnector</tt> to close
*/
public void closeStreamConnector(MediaType mediaType)
{
int index = mediaType.ordinal();
StreamConnector streamConnector = streamConnectors[index];
if (streamConnector != null)
{
closeStreamConnector(mediaType, streamConnector);
streamConnectors[index] = null;
}
}
/**
* Closes a specific <tt>StreamConnector</tt> associated with a specific
* <tt>MediaType</tt>. If this <tt>TransportManager</tt> has a reference to
* the specified <tt>streamConnector</tt>, it remains. Allows extenders to
* override and perform additional customizations to the closing of the
* specified <tt>streamConnector</tt>.
*
* @param mediaType the <tt>MediaType</tt> associated with the specified
* <tt>streamConnector</tt>
* @param streamConnector the <tt>StreamConnector</tt> to be closed
* @see #closeStreamConnector(MediaType)
*/
protected void closeStreamConnector(
MediaType mediaType,
StreamConnector streamConnector)
{
/*
* XXX The connected owns the sockets so it is important that it
* decides whether to close them i.e. this TransportManager is not
* allowed to explicitly close the sockets by itself.
*/
streamConnector.close();
}
/**
* Creates a media <tt>StreamConnector</tt> for a stream of a specific
* <tt>MediaType</tt>. The minimum and maximum of the media port boundaries
* are taken into account.
*
* @param mediaType the <tt>MediaType</tt> of the stream for which a
* <tt>StreamConnector</tt> is to be created
* @return a <tt>StreamConnector</tt> for the stream of the specified
* <tt>mediaType</tt>
* @throws OperationFailedException if the binding of the sockets fails
*/
protected StreamConnector createStreamConnector(MediaType mediaType)
throws OperationFailedException
{
NetworkAddressManagerService nam
= ProtocolMediaActivator.getNetworkAddressManagerService();
InetAddress intendedDestination = getIntendedDestination(getCallPeer());
InetAddress localHostForPeer = nam.getLocalHost(intendedDestination);
PortTracker portTracker = getPortTracker(mediaType);
//create the RTP socket.
DatagramSocket rtpSocket = null;
try
{
rtpSocket = nam.createDatagramSocket(
localHostForPeer, portTracker.getPort(),
portTracker.getMinPort(), portTracker.getMaxPort());
}
catch (Exception exc)
{
throw new OperationFailedException(
"Failed to allocate the network ports necessary for the call.",
OperationFailedException.INTERNAL_ERROR, exc);
}
//make sure that next time we don't try to bind on occupied ports
//also, refuse validation in case someone set the tracker range to 1
portTracker.setNextPort( rtpSocket.getLocalPort() + 1, false);
//create the RTCP socket, preferably on the port following our RTP one.
DatagramSocket rtcpSocket = null;
try
{
rtcpSocket = nam.createDatagramSocket(
localHostForPeer, portTracker.getPort(),
portTracker.getMinPort(), portTracker.getMaxPort());
}
catch (Exception exc)
{
throw new OperationFailedException(
"Failed to allocate the network ports necessary for the call.",
OperationFailedException.INTERNAL_ERROR,
exc);
}
//make sure that next time we don't try to bind on occupied ports
portTracker.setNextPort( rtcpSocket.getLocalPort() + 1);
return new DefaultStreamConnector(rtpSocket, rtcpSocket);
}
/**
* Tries to set the ranges of the <tt>PortTracker</tt>s (e.g. default,
* audio, video, data channel) to the values specified in the
* <tt>ConfigurationService</tt>.
*/
protected synchronized static void initializePortNumbers()
{
//try the default tracker first
ConfigurationService cfg
= ProtocolMediaActivator.getConfigurationService();
String minPort, maxPort;
minPort
= cfg.getString(
OperationSetBasicTelephony
.MIN_MEDIA_PORT_NUMBER_PROPERTY_NAME);
if (minPort != null)
{
maxPort
= cfg.getString(
OperationSetBasicTelephony
.MAX_MEDIA_PORT_NUMBER_PROPERTY_NAME);
if (maxPort != null)
{
//Try the specified range; otherwise, leave the tracker as it
//is: [5000, 6000].
defaultPortTracker.tryRange(minPort, maxPort);
}
}
//try the VIDEO tracker
minPort
= cfg.getString(
OperationSetBasicTelephony
.MIN_VIDEO_PORT_NUMBER_PROPERTY_NAME);
if (minPort != null)
{
maxPort
= cfg.getString(
OperationSetBasicTelephony
.MAX_VIDEO_PORT_NUMBER_PROPERTY_NAME);
if (maxPort != null)
{
//Try the specified range; otherwise, leave the tracker to null.
if (videoPortTracker == null)
{
videoPortTracker
= PortTracker.createTracker(minPort, maxPort);
}
else
{
videoPortTracker.tryRange(minPort, maxPort);
}
}
}
//try the AUDIO tracker
minPort
= cfg.getString(
OperationSetBasicTelephony
.MIN_AUDIO_PORT_NUMBER_PROPERTY_NAME);
if (minPort != null)
{
maxPort
= cfg.getString(
OperationSetBasicTelephony
.MAX_AUDIO_PORT_NUMBER_PROPERTY_NAME);
if (maxPort != null)
{
//Try the specified range; otherwise, leave the tracker to null.
if (audioPortTracker == null)
{
audioPortTracker
= PortTracker.createTracker(minPort, maxPort);
}
else
{
audioPortTracker.tryRange(minPort, maxPort);
}
}
}
//try the DATA CHANNEL tracker
minPort
= cfg.getString(
OperationSetBasicTelephony
.MIN_DATA_CHANNEL_PORT_NUMBER_PROPERTY_NAME);
if (minPort != null)
{
maxPort
= cfg.getString(
OperationSetBasicTelephony
.MAX_DATA_CHANNEL_PORT_NUMBER_PROPERTY_NAME);
if (maxPort != null)
{
//Try the specified range; otherwise, leave the tracker to null.
if (dataPortTracker == null)
{
dataPortTracker
= PortTracker.createTracker(minPort, maxPort);
}
else
{
dataPortTracker.tryRange(minPort, maxPort);
}
}
}
}
/**
* Returns the <tt>InetAddress</tt> that we are using in one of our
* <tt>StreamConnector</tt>s or, in case we don't have any connectors yet
* the address returned by the our network address manager as the best local
* address to use when contacting the <tt>CallPeer</tt> associated with this
* <tt>MediaHandler</tt>. This method is primarily meant for use with the
* o= and c= fields of a newly created session description. The point is
* that we create our <tt>StreamConnector</tt>s when constructing the media
* descriptions so we already have a specific local address assigned to them
* at the time we get ready to create the c= and o= fields. It is therefore
* better to try and return one of these addresses before trying the net
* address manager again and running the slight risk of getting a different
* address.
*
* @return an <tt>InetAddress</tt> that we use in one of the
* <tt>StreamConnector</tt>s in this class.
*/
public InetAddress getLastUsedLocalHost()
{
for (MediaType mediaType : MediaType.values())
{
StreamConnector streamConnector
= streamConnectors[mediaType.ordinal()];
if (streamConnector != null)
return streamConnector.getDataSocket().getLocalAddress();
}
NetworkAddressManagerService nam
= ProtocolMediaActivator.getNetworkAddressManagerService();
InetAddress intendedDestination = getIntendedDestination(getCallPeer());
return nam.getLocalHost(intendedDestination);
}
/**
* Sends empty UDP packets to target destination data/control ports in order
* to open ports on NATs or and help RTP proxies latch onto our RTP ports.
*
* @param target <tt>MediaStreamTarget</tt>
* @param type the {@link MediaType} of the connector we'd like to send the
* hole punching packet through.
*/
public void sendHolePunchPacket(MediaStreamTarget target, MediaType type)
{
this.sendHolePunchPacket(target, type, null);
}
/**
* Sends empty UDP packets to target destination data/control ports in order
* to open ports on NATs or and help RTP proxies latch onto our RTP ports.
*
* @param target <tt>MediaStreamTarget</tt>
* @param type the {@link MediaType} of the connector we'd like to send the
* hole punching packet through.
* @param packet (optional) use a pre-generated packet that will be sent
*/
public void sendHolePunchPacket(
MediaStreamTarget target, MediaType type, RawPacket packet)
{
logger.info("Send NAT hole punch packets");
//check how many hole punch packets we would be supposed to send:
int packetCount
= ProtocolMediaActivator.getConfigurationService().getInt(
HOLE_PUNCH_PKT_COUNT_PROPERTY,
DEFAULT_HOLE_PUNCH_PKT_COUNT);
if (packetCount < 0)
packetCount = DEFAULT_HOLE_PUNCH_PKT_COUNT;
if (packetCount == 0)
return;
try
{
final StreamConnector connector = getStreamConnector(type);
if(connector.getProtocol() == StreamConnector.Protocol.TCP)
return;
byte[] buf;
if (packet != null)
buf = packet.getBuffer();
else
buf = new byte[0];
synchronized(connector)
{
//we may want to send more than one packet in case they get lost
for(int i=0; i < packetCount; i++)
{
DatagramSocket socket;
// data/RTP
if((socket = connector.getDataSocket()) != null)
{
InetSocketAddress dataAddress = target.getDataAddress();
socket.send(
new DatagramPacket(
buf,
buf.length,
dataAddress.getAddress(),
dataAddress.getPort()));
}
// control/RTCP
if((socket = connector.getControlSocket()) != null)
{
InetSocketAddress controlAddress
= target.getControlAddress();
socket.send(
new DatagramPacket(
buf,
buf.length,
controlAddress.getAddress(),
controlAddress.getPort()));
}
}
}
}
catch(Exception e)
{
logger.error("Error cannot send to remote peer", e);
}
}
/**
* Set traffic class (QoS) for the RTP socket.
*
* @param target <tt>MediaStreamTarget</tt>
* @param type the {@link MediaType} of the connector we'd like to set
* traffic class
*/
protected void setTrafficClass(MediaStreamTarget target, MediaType type)
{
// get traffic class value for RTP audio/video
int trafficClass = getDSCP(type);
if(trafficClass <= 0)
return;
if (logger.isInfoEnabled())
logger.info(
"Set traffic class for " + type + " to " + trafficClass);
try
{
final StreamConnector connector = getStreamConnector(type);
synchronized(connector)
{
if(connector.getProtocol() == StreamConnector.Protocol.TCP)
{
connector.getDataTCPSocket().setTrafficClass(trafficClass);
Socket controlTCPSocket = connector.getControlTCPSocket();
if (controlTCPSocket != null)
controlTCPSocket.setTrafficClass(trafficClass);
}
else
{
/* data port (RTP) */
connector.getDataSocket().setTrafficClass(trafficClass);
/* control port (RTCP) */
DatagramSocket controlSocket = connector.getControlSocket();
if (controlSocket != null)
controlSocket.setTrafficClass(trafficClass);
}
}
}
catch(Exception e)
{
logger.error(
"Failed to set traffic class for " + type + " to "
+ trafficClass,
e);
}
}
/**
* Gets the SIP traffic class associated with a specific <tt>MediaType</tt>
* from the configuration.
*
* @param type the <tt>MediaType</tt> to get the associated SIP traffic
* class of
* @return the SIP traffic class associated with the specified
* <tt>MediaType</tt> or <tt>0</tt> if not configured
*/
private int getDSCP(MediaType type)
{
String dscpPropertyName;
switch (type)
{
case AUDIO:
dscpPropertyName = RTP_AUDIO_DSCP_PROPERTY;
break;
case VIDEO:
dscpPropertyName = RTP_VIDEO_DSCP_PROPERTY;
break;
default:
dscpPropertyName = null;
break;
}
return
(dscpPropertyName == null)
? 0
: (ProtocolMediaActivator.getConfigurationService().getInt(
dscpPropertyName,
0)
<< 2);
}
/**
* Returns the <tt>InetAddress</tt> that is most likely to be used as a
* next hop when contacting the specified <tt>destination</tt>. This is
* an utility method that is used whenever we have to choose one of our
* local addresses to put in the Via, Contact or (in the case of no
* registrar accounts) From headers.
*
* @param peer the CallPeer that we would contact.
*
* @return the <tt>InetAddress</tt> that is most likely to be to be used
* as a next hop when contacting the specified <tt>destination</tt>.
*
* @throws IllegalArgumentException if <tt>destination</tt> is not a valid
* host/ip/fqdn
*/
protected abstract InetAddress getIntendedDestination(U peer);
/**
* Returns the {@link MediaAwareCallPeer} that this transport manager is
* serving.
*
* @return the {@link MediaAwareCallPeer} that this transport manager is
* serving.
*/
public U getCallPeer()
{
return callPeer;
}
/**
* Returns the extended type of the candidate selected if this transport
* manager is using ICE.
*
* @param streamName The stream name (AUDIO, VIDEO);
*
* @return The extended type of the candidate selected if this transport
* manager is using ICE. Otherwise, returns null.
*/
public abstract String getICECandidateExtendedType(String streamName);
/**
* Returns the current state of ICE processing.
*
* @return the current state of ICE processing if this transport
* manager is using ICE. Otherwise, returns null.
*/
public abstract String getICEState();
/**
* Returns the ICE local host address.
*
* @param streamName The stream name (AUDIO, VIDEO);
*
* @return the ICE local host address if this transport
* manager is using ICE. Otherwise, returns null.
*/
public abstract InetSocketAddress getICELocalHostAddress(String streamName);
/**
* Returns the ICE remote host address.
*
* @param streamName The stream name (AUDIO, VIDEO);
*
* @return the ICE remote host address if this transport
* manager is using ICE. Otherwise, returns null.
*/
public abstract InetSocketAddress getICERemoteHostAddress(
String streamName);
/**
* Returns the ICE local reflexive address (server or peer reflexive).
*
* @param streamName The stream name (AUDIO, VIDEO);
*
* @return the ICE local reflexive address. May be null if this transport
* manager is not using ICE or if there is no reflexive address for the
* local candidate used.
*/
public abstract InetSocketAddress getICELocalReflexiveAddress(
String streamName);
/**
* Returns the ICE remote reflexive address (server or peer reflexive).
*
* @param streamName The stream name (AUDIO, VIDEO);
*
* @return the ICE remote reflexive address. May be null if this transport
* manager is not using ICE or if there is no reflexive address for the
* remote candidate used.
*/
public abstract InetSocketAddress getICERemoteReflexiveAddress(
String streamName);
/**
* Returns the ICE local relayed address (server or peer relayed).
*
* @param streamName The stream name (AUDIO, VIDEO);
*
* @return the ICE local relayed address. May be null if this transport
* manager is not using ICE or if there is no relayed address for the
* local candidate used.
*/
public abstract InetSocketAddress getICELocalRelayedAddress(
String streamName);
/**
* Returns the ICE remote relayed address (server or peer relayed).
*
* @param streamName The stream name (AUDIO, VIDEO);
*
* @return the ICE remote relayed address. May be null if this transport
* manager is not using ICE or if there is no relayed address for the
* remote candidate used.
*/
public abstract InetSocketAddress getICERemoteRelayedAddress(
String streamName);
/**
* Returns the total harvesting time (in ms) for all harvesters.
*
* @return The total harvesting time (in ms) for all the harvesters. 0 if
* the ICE agent is null, or if the agent has nevers harvested.
*/
public abstract long getTotalHarvestingTime();
/**
* Returns the harvesting time (in ms) for the harvester given in parameter.
*
* @param harvesterName The class name if the harvester.
*
* @return The harvesting time (in ms) for the harvester given in parameter.
* 0 if this harvester does not exists, if the ICE agent is null, or if the
* agent has never harvested with this harvester.
*/
public abstract long getHarvestingTime(String harvesterName);
/**
* Returns the number of harvesting for this agent.
*
* @return The number of harvesting for this agent.
*/
public abstract int getNbHarvesting();
/**
* Returns the number of harvesting time for the harvester given in
* parameter.
*
* @param harvesterName The class name if the harvester.
*
* @return The number of harvesting time for the harvester given in
* parameter.
*/
public abstract int getNbHarvesting(String harvesterName);
/**
* Returns the ICE candidate extended type selected by the given agent.
*
* @param iceAgent The ICE agent managing the ICE offer/answer exchange,
* collecting and selecting the candidate.
* @param streamName The stream name (AUDIO, VIDEO);
*
* @return The ICE candidate extended type selected by the given agent. null
* if the iceAgent is null or if there is no candidate selected or
* available.
*/
public static String getICECandidateExtendedType(
Agent iceAgent,
String streamName)
{
if(iceAgent != null)
{
LocalCandidate localCandidate
= iceAgent.getSelectedLocalCandidate(streamName);
if(localCandidate != null)
return localCandidate.getExtendedType().toString();
}
return null;
}
/**
* Creates the ICE agent that we would be using in this transport manager
* for all negotiation.
*
* @return the ICE agent to use for all the ICE negotiation that this
* transport manager would be going through
*/
protected Agent createIceAgent()
{
//work in progress
return null;
}
/**
* Creates an {@link IceMediaStream} with the specified <tt>media</tt>
* name.
*
* @param media the name of the stream we'd like to create.
* @param agent the ICE {@link Agent} that we will be appending the stream
* to.
*
* @return the newly created {@link IceMediaStream}
*
* @throws OperationFailedException if binding on the specified media stream
* fails for some reason.
*/
protected IceMediaStream createIceStream(String media, Agent agent)
throws OperationFailedException
{
return null;
}
}