/*
* 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.impl.protocol.jabber;
import java.lang.ref.*;
import java.util.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*;
import net.java.sip.communicator.impl.protocol.jabber.jinglesdp.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.service.protocol.media.*;
import net.java.sip.communicator.util.*;
import org.jitsi.service.neomedia.*;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.filter.*;
import org.jivesoftware.smack.packet.*;
import org.jivesoftware.smackx.packet.*;
/**
* A Jabber implementation of the <tt>Call</tt> abstract class encapsulating
* Jabber jingle sessions.
*
* @author Emil Ivov
* @author Lyubomir Marinov
* @author Boris Grozev
*/
public class CallJabberImpl
extends AbstractCallJabberGTalkImpl<CallPeerJabberImpl>
{
/**
* The <tt>Logger</tt> used by the <tt>CallJabberImpl</tt> class and its
* instances for logging output.
*/
private static final Logger logger = Logger.getLogger(CallJabberImpl.class);
/**
* The Jitsi Videobridge conference which the local peer represented by this
* instance is a focus of.
*/
private ColibriConferenceIQ colibri;
/**
* The shared <tt>CallPeerMediaHandler</tt> state which is to be used by the
* <tt>CallPeer</tt>s of this <tt>Call</tt> which use {@link #colibri}.
*/
private MediaHandler colibriMediaHandler;
/**
* Contains one ColibriStreamConnector for each <tt>MediaType</tt>
*/
private final List<WeakReference<ColibriStreamConnector>>
colibriStreamConnectors;
/**
* The entity ID of the Jitsi Videobridge to be utilized by this
* <tt>Call</tt> for the purposes of establishing a server-assisted
* telephony conference.
*/
private String jitsiVideobridge;
/**
* Initializes a new <tt>CallJabberImpl</tt> instance.
*
* @param parentOpSet the {@link OperationSetBasicTelephonyJabberImpl}
* instance in the context of which this call has been created.
*/
protected CallJabberImpl(
OperationSetBasicTelephonyJabberImpl parentOpSet)
{
super(parentOpSet);
int mediaTypeValueCount = MediaType.values().length;
colibriStreamConnectors
= new ArrayList<WeakReference<ColibriStreamConnector>>(
mediaTypeValueCount);
for (int i = 0; i < mediaTypeValueCount; i++)
colibriStreamConnectors.add(null);
//let's add ourselves to the calls repo. we are doing it ourselves just
//to make sure that no one ever forgets.
parentOpSet.getActiveCallsRepository().addCall(this);
}
/**
* Closes a specific <tt>ColibriStreamConnector</tt> which is associated with
* a <tt>MediaStream</tt> of a specific <tt>MediaType</tt> upon request from
* a specific <tt>CallPeer</tt>.
*
* @param peer the <tt>CallPeer</tt> which requests the closing of the
* specified <tt>colibriStreamConnector</tt>
* @param mediaType the <tt>MediaType</tt> of the <tt>MediaStream</tt> with
* which the specified <tt>colibriStreamConnector</tt> is associated
* @param colibriStreamConnector the <tt>ColibriStreamConnector</tt> to close on
* behalf of the specified <tt>peer</tt>
*/
public void closeColibriStreamConnector(
CallPeerJabberImpl peer,
MediaType mediaType,
ColibriStreamConnector colibriStreamConnector)
{
colibriStreamConnector.close();
synchronized (colibriStreamConnectors)
{
int index = mediaType.ordinal();
WeakReference<ColibriStreamConnector> weakReference
= colibriStreamConnectors.get(index);
if (weakReference != null && colibriStreamConnector
.equals(weakReference.get()))
{
colibriStreamConnectors.set(index, null);
}
}
}
/**
* {@inheritDoc}
*
* Sends a <tt>content</tt> message to each of the <tt>CallPeer</tt>s
* associated with this <tt>CallJabberImpl</tt> in order to include/exclude
* the "isfocus" attribute.
*/
@Override
protected void conferenceFocusChanged(boolean oldValue, boolean newValue)
{
try
{
Iterator<CallPeerJabberImpl> peers = getCallPeers();
while (peers.hasNext())
{
CallPeerJabberImpl callPeer = peers.next();
if (callPeer.getState() == CallPeerState.CONNECTED)
callPeer.sendCoinSessionInfo();
}
}
finally
{
super.conferenceFocusChanged(oldValue, newValue);
}
}
/**
* Allocates colibri (conference) channels for a specific <tt>MediaType</tt>
* to be used by a specific <tt>CallPeer</tt>.
*
* @param peer the <tt>CallPeer</tt> which is to use the allocated colibri
* (conference) channels
* @param contentMap the local and remote <tt>ContentPacketExtension</tt>s
* which specify the <tt>MediaType</tt>s for which colibri (conference)
* channels are to be allocated
* @return a <tt>ColibriConferenceIQ</tt> which describes the allocated
* colibri (conference) channels for the specified <tt>mediaTypes</tt> which
* are to be used by the specified <tt>peer</tt>; otherwise, <tt>null</tt>
*/
public ColibriConferenceIQ createColibriChannels(
CallPeerJabberImpl peer,
Map<ContentPacketExtension,ContentPacketExtension> contentMap)
throws OperationFailedException
{
if (!getConference().isJitsiVideobridge())
return null;
/*
* For a colibri conference to work properly, all CallPeers in the
* conference must share one and the same CallPeerMediaHandler state
* i.e. they must use a single set of MediaStreams as if there was a
* single CallPeerMediaHandler.
*/
CallPeerMediaHandlerJabberImpl peerMediaHandler
= peer.getMediaHandler();
if (peerMediaHandler.getMediaHandler() != colibriMediaHandler)
{
for (MediaType mediaType : MediaType.values())
{
if (peerMediaHandler.getStream(mediaType) != null)
return null;
}
}
ProtocolProviderServiceJabberImpl protocolProvider
= getProtocolProvider();
String jitsiVideobridge
= (colibri == null) ? getJitsiVideobridge() : colibri.getFrom();
if ((jitsiVideobridge == null) || (jitsiVideobridge.length() == 0))
{
logger.error(
"Failed to allocate colibri channels: no videobridge"
+ " found.");
return null;
}
/*
* The specified CallPeer will participate in the colibri conference
* organized by this Call so it must use the shared CallPeerMediaHandler
* state of all CallPeers in the same colibri conference.
*/
if (colibriMediaHandler == null)
colibriMediaHandler = new MediaHandler();
peerMediaHandler.setMediaHandler(colibriMediaHandler);
ColibriConferenceIQ conferenceRequest = new ColibriConferenceIQ();
if (colibri != null)
conferenceRequest.setID(colibri.getID());
for (Map.Entry<ContentPacketExtension,ContentPacketExtension> e
: contentMap.entrySet())
{
ContentPacketExtension localContent = e.getKey();
ContentPacketExtension remoteContent = e.getValue();
ContentPacketExtension cpe
= (remoteContent == null) ? localContent : remoteContent;
RtpDescriptionPacketExtension rdpe
= cpe.getFirstChildOfType(
RtpDescriptionPacketExtension.class);
String media = rdpe.getMedia();
MediaType mediaType = MediaType.parseString(media);
String contentName = mediaType.toString();
ColibriConferenceIQ.Content contentRequest
= new ColibriConferenceIQ.Content(contentName);
conferenceRequest.addContent(contentRequest);
boolean requestLocalChannel = true;
if (colibri != null)
{
ColibriConferenceIQ.Content content
= colibri.getContent(contentName);
if ((content != null) && (content.getChannelCount() > 0))
requestLocalChannel = false;
}
boolean peerIsInitiator = peer.isInitiator();
if (requestLocalChannel)
{
ColibriConferenceIQ.Channel localChannelRequest
= new ColibriConferenceIQ.Channel();
localChannelRequest.setEndpoint(protocolProvider.getOurJID());
localChannelRequest.setInitiator(peerIsInitiator);
for (PayloadTypePacketExtension ptpe : rdpe.getPayloadTypes())
localChannelRequest.addPayloadType(ptpe);
setTransportOnChannel(peer, media, localChannelRequest);
// DTLS-SRTP
setDtlsEncryptionOnChannel(
jitsiVideobridge,
peer,
mediaType,
localChannelRequest);
/*
* Since Jitsi Videobridge supports multiple Jingle transports,
* it is a good idea to indicate which one is expected on a
* channel.
*/
ensureTransportOnChannel(localChannelRequest, peer);
contentRequest.addChannel(localChannelRequest);
}
ColibriConferenceIQ.Channel remoteChannelRequest
= new ColibriConferenceIQ.Channel();
remoteChannelRequest.setEndpoint(peer.getAddress());
remoteChannelRequest.setInitiator(!peerIsInitiator);
for (PayloadTypePacketExtension ptpe : rdpe.getPayloadTypes())
remoteChannelRequest.addPayloadType(ptpe);
setTransportOnChannel(
media,
localContent,
remoteContent,
peer,
remoteChannelRequest);
// DTLS-SRTP
setDtlsEncryptionOnChannel(
mediaType,
localContent,
remoteContent,
peer,
remoteChannelRequest);
/*
* Since Jitsi Videobridge supports multiple Jingle transports, it
* is a good idea to indicate which one is expected on a channel.
*/
ensureTransportOnChannel(remoteChannelRequest, peer);
contentRequest.addChannel(remoteChannelRequest);
}
Connection connection = protocolProvider.getConnection();
PacketCollector packetCollector
= connection.createPacketCollector(
new PacketIDFilter(conferenceRequest.getPacketID()));
conferenceRequest.setTo(jitsiVideobridge);
conferenceRequest.setType(IQ.Type.GET);
connection.sendPacket(conferenceRequest);
Packet response
= packetCollector.nextResult(
SmackConfiguration.getPacketReplyTimeout());
packetCollector.cancel();
if (response == null)
{
logger.error(
"Failed to allocate colibri channels: response is null."
+ " Maybe the response timed out.");
return null;
}
else if (response.getError() != null)
{
logger.error(
"Failed to allocate colibri channels: "
+ response.getError());
return null;
}
else if (!(response instanceof ColibriConferenceIQ))
{
logger.error(
"Failed to allocate colibri channels: response is not a"
+ " colibri conference");
return null;
}
ColibriConferenceIQ conferenceResponse = (ColibriConferenceIQ) response;
String conferenceResponseID = conferenceResponse.getID();
/*
* Update the complete ColibriConferenceIQ representation maintained by
* this instance with the information given by the (current) response.
*/
{
if (colibri == null)
{
colibri = new ColibriConferenceIQ();
/*
* XXX We must remember the JID of the Jitsi Videobridge because
* (1) we do not want to re-discover it in every method
* invocation on this Call instance and (2) we want to use one
* and the same for all CallPeers within this Call instance.
*/
colibri.setFrom(conferenceResponse.getFrom());
}
String colibriID = colibri.getID();
if (colibriID == null)
colibri.setID(conferenceResponseID);
else if (!colibriID.equals(conferenceResponseID))
throw new IllegalStateException("conference.id");
for (ColibriConferenceIQ.Content contentResponse
: conferenceResponse.getContents())
{
String contentName = contentResponse.getName();
ColibriConferenceIQ.Content content
= colibri.getOrCreateContent(contentName);
for (ColibriConferenceIQ.Channel channelResponse
: contentResponse.getChannels())
{
int channelIndex = content.getChannelCount();
content.addChannel(channelResponse);
if (channelIndex == 0)
{
TransportManagerJabberImpl transportManager
= peerMediaHandler.getTransportManager();
transportManager
.isEstablishingConnectivityWithJitsiVideobridge
= true;
transportManager
.startConnectivityEstablishmentWithJitsiVideobridge
= true;
MediaType mediaType
= MediaType.parseString(contentName);
// DTLS-SRTP
addDtlsAdvertisedEncryptions(
peer,
channelResponse,
mediaType);
}
}
}
}
/*
* Formulate the result to be returned to the caller which is a subset
* of the whole conference information kept by this CallJabberImpl and
* includes the remote channels explicitly requested by the method
* caller and their respective local channels.
*/
ColibriConferenceIQ conferenceResult = new ColibriConferenceIQ();
conferenceResult.setFrom(colibri.getFrom());
conferenceResult.setID(conferenceResponseID);
for (Map.Entry<ContentPacketExtension,ContentPacketExtension> e
: contentMap.entrySet())
{
ContentPacketExtension localContent = e.getKey();
ContentPacketExtension remoteContent = e.getValue();
ContentPacketExtension cpe
= (remoteContent == null) ? localContent : remoteContent;
MediaType mediaType = JingleUtils.getMediaType(cpe);
ColibriConferenceIQ.Content contentResponse
= conferenceResponse.getContent(mediaType.toString());
if (contentResponse != null)
{
String contentName = contentResponse.getName();
ColibriConferenceIQ.Content contentResult
= new ColibriConferenceIQ.Content(contentName);
conferenceResult.addContent(contentResult);
/*
* The local channel may have been allocated in a previous
* method call as part of the allocation of the first remote
* channel in the respective content. Anyway, the current method
* caller still needs to know about it.
*/
ColibriConferenceIQ.Content content
= colibri.getContent(contentName);
ColibriConferenceIQ.Channel localChannel = null;
if ((content != null) && (content.getChannelCount() > 0))
{
localChannel = content.getChannel(0);
contentResult.addChannel(localChannel);
}
String localChannelID
= (localChannel == null) ? null : localChannel.getID();
for (ColibriConferenceIQ.Channel channelResponse
: contentResponse.getChannels())
{
if ((localChannelID == null)
|| !localChannelID.equals(channelResponse.getID()))
contentResult.addChannel(channelResponse);
}
}
}
return conferenceResult;
}
/**
* Initializes a <tt>ColibriStreamConnector</tt> on behalf of a specific
* <tt>CallPeer</tt> to be used in association with a specific
* <tt>ColibriConferenceIQ.Channel</tt> of a specific <tt>MediaType</tt>.
*
* @param peer the <tt>CallPeer</tt> which requests the initialization of a
* <tt>ColibriStreamConnector</tt>
* @param mediaType the <tt>MediaType</tt> of the stream which is to use the
* initialized <tt>ColibriStreamConnector</tt> for RTP and RTCP traffic
* @param channel the <tt>ColibriConferenceIQ.Channel</tt> to which RTP and
* RTCP traffic is to be sent and from which such traffic is to be received
* via the initialized <tt>ColibriStreamConnector</tt>
* @param factory a <tt>StreamConnectorFactory</tt> implementation which is
* to allocate the sockets to be used for RTP and RTCP traffic
* @return a <tt>ColibriStreamConnector</tt> to be used for RTP and RTCP
* traffic associated with the specified <tt>channel</tt>
*/
public ColibriStreamConnector createColibriStreamConnector(
CallPeerJabberImpl peer,
MediaType mediaType,
ColibriConferenceIQ.Channel channel,
StreamConnectorFactory factory)
{
String channelID = channel.getID();
if (channelID == null)
throw new IllegalArgumentException("channel");
if (colibri == null)
throw new IllegalStateException("colibri");
ColibriConferenceIQ.Content content
= colibri.getContent(mediaType.toString());
if (content == null)
throw new IllegalArgumentException("mediaType");
if ((content.getChannelCount() < 1)
|| !channelID.equals((channel = content.getChannel(0)).getID()))
throw new IllegalArgumentException("channel");
ColibriStreamConnector colibriStreamConnector;
synchronized (colibriStreamConnectors)
{
int index = mediaType.ordinal();
WeakReference<ColibriStreamConnector> weakReference
= colibriStreamConnectors.get(index);
colibriStreamConnector
= (weakReference == null) ? null : weakReference.get();
if (colibriStreamConnector == null)
{
StreamConnector streamConnector
= factory.createStreamConnector();
if (streamConnector != null)
{
colibriStreamConnector
= new ColibriStreamConnector(streamConnector);
colibriStreamConnectors.set(
index,
new WeakReference<ColibriStreamConnector>(
colibriStreamConnector));
}
}
}
return colibriStreamConnector;
}
/**
* Expires specific (colibri) conference channels used by a specific
* <tt>CallPeer</tt>.
*
* @param peer the <tt>CallPeer</tt> which uses the specified (colibri)
* conference channels to be expired
* @param conference a <tt>ColibriConferenceIQ</tt> which specifies the
* (colibri) conference channels to be expired
*/
public void expireColibriChannels(
CallPeerJabberImpl peer,
ColibriConferenceIQ conference)
{
// Formulate the ColibriConferenceIQ request which is to be sent.
if (colibri != null)
{
String conferenceID = colibri.getID();
if (conferenceID.equals(conference.getID()))
{
ColibriConferenceIQ conferenceRequest
= new ColibriConferenceIQ();
conferenceRequest.setID(conferenceID);
for (ColibriConferenceIQ.Content content
: conference.getContents())
{
ColibriConferenceIQ.Content colibriContent
= colibri.getContent(content.getName());
if (colibriContent != null)
{
ColibriConferenceIQ.Content contentRequest
= conferenceRequest.getOrCreateContent(
colibriContent.getName());
for (ColibriConferenceIQ.Channel channel
: content.getChannels())
{
ColibriConferenceIQ.Channel colibriChannel
= colibriContent.getChannel(channel.getID());
if (colibriChannel != null)
{
ColibriConferenceIQ.Channel channelRequest
= new ColibriConferenceIQ.Channel();
channelRequest.setExpire(0);
channelRequest.setID(colibriChannel.getID());
contentRequest.addChannel(channelRequest);
}
}
}
}
/*
* Remove the channels which are to be expired from the internal
* state of the conference managed by this CallJabberImpl.
*/
for (ColibriConferenceIQ.Content contentRequest
: conferenceRequest.getContents())
{
ColibriConferenceIQ.Content colibriContent
= colibri.getContent(contentRequest.getName());
for (ColibriConferenceIQ.Channel channelRequest
: contentRequest.getChannels())
{
ColibriConferenceIQ.Channel colibriChannel
= colibriContent.getChannel(channelRequest.getID());
colibriContent.removeChannel(colibriChannel);
/*
* If the last remote channel is to be expired, expire
* the local channel as well.
*/
if (colibriContent.getChannelCount() == 1)
{
colibriChannel = colibriContent.getChannel(0);
channelRequest = new ColibriConferenceIQ.Channel();
channelRequest.setExpire(0);
channelRequest.setID(colibriChannel.getID());
contentRequest.addChannel(channelRequest);
colibriContent.removeChannel(colibriChannel);
break;
}
}
}
/*
* At long last, send the ColibriConferenceIQ request to expire
* the channels.
*/
conferenceRequest.setTo(colibri.getFrom());
conferenceRequest.setType(IQ.Type.SET);
getProtocolProvider().getConnection().sendPacket(
conferenceRequest);
}
}
}
/**
* Sends a <tt>ColibriConferenceIQ</tt> to the videobridge used by this
* <tt>CallJabberImpl</tt>, in order to request the the direction of
* the <tt>channel</tt> with ID <tt>channelID</tt> be set to
* <tt>direction</tt>
* @param channelID the ID of the <tt>channel</tt> for which to set the
* direction.
* @param mediaType the <tt>MediaType</tt> of the channel (we can deduce this
* by searching the <tt>ColibriConferenceIQ</tt>, but it's more convenient
* to have it)
* @param direction the <tt>MediaDirection</tt> to set.
*/
public void setChannelDirection(String channelID,
MediaType mediaType,
MediaDirection direction)
{
if ((colibri != null) && (channelID != null))
{
ColibriConferenceIQ.Content content
= colibri.getContent(mediaType.toString());
if (content != null)
{
ColibriConferenceIQ.Channel channel
= content.getChannel(channelID);
/*
* Note that we send requests even when the local Channel's
* direction and the direction we are setting are the same. We
* can easily avoid this, but we risk not sending necessary
* packets if local Channel and the actual channel on the
* videobridge are out of sync.
*/
if (channel != null)
{
ColibriConferenceIQ.Channel requestChannel
= new ColibriConferenceIQ.Channel();
requestChannel.setID(channelID);
requestChannel.setDirection(direction);
ColibriConferenceIQ.Content requestContent
= new ColibriConferenceIQ.Content();
requestContent.setName(mediaType.toString());
requestContent.addChannel(requestChannel);
ColibriConferenceIQ conferenceRequest
= new ColibriConferenceIQ();
conferenceRequest.setID(colibri.getID());
conferenceRequest.setTo(colibri.getFrom());
conferenceRequest.setType(IQ.Type.SET);
conferenceRequest.addContent(requestContent);
getProtocolProvider().getConnection().sendPacket(
conferenceRequest);
}
}
}
}
/**
* Creates a <tt>CallPeerJabberImpl</tt> from <tt>calleeJID</tt> and sends
* them <tt>session-initiate</tt> IQ request.
*
* @param calleeJID the party that we would like to invite to this call.
* @param discoverInfo any discovery information that we have for the jid
* we are trying to reach and that we are passing in order to avoid having
* to ask for it again.
* @param sessionInitiateExtensions a collection of additional and optional
* <tt>PacketExtension</tt>s to be added to the <tt>session-initiate</tt>
* {@link JingleIQ} which is to init this <tt>CallJabberImpl</tt>
* @param supportedTransports the XML namespaces of the jingle transports
* to use.
*
* @return the newly created <tt>CallPeerJabberImpl</tt> corresponding to
* <tt>calleeJID</tt>. All following state change events will be
* delivered through this call peer.
*
* @throws OperationFailedException with the corresponding code if we fail
* to create the call.
*/
public CallPeerJabberImpl initiateSession(
String calleeJID,
DiscoverInfo discoverInfo,
Iterable<PacketExtension> sessionInitiateExtensions,
Collection<String> supportedTransports)
throws OperationFailedException
{
// create the session-initiate IQ
CallPeerJabberImpl callPeer = new CallPeerJabberImpl(calleeJID, this);
callPeer.setDiscoveryInfo(discoverInfo);
addCallPeer(callPeer);
callPeer.setState(CallPeerState.INITIATING_CALL);
// If this was the first peer we added in this call, then the call is
// new and we need to notify everyone of its creation.
if (getCallPeerCount() == 1)
parentOpSet.fireCallEvent(CallEvent.CALL_INITIATED, this);
CallPeerMediaHandlerJabberImpl mediaHandler
= callPeer.getMediaHandler();
//set the supported transports before the transport manager is created
mediaHandler.setSupportedTransports(supportedTransports);
/* enable video if it is a video call */
mediaHandler.setLocalVideoTransmissionEnabled(localVideoAllowed);
/* enable remote-control if it is a desktop sharing session */
mediaHandler.setLocalInputEvtAware(getLocalInputEvtAware());
/*
* Set call state to connecting so that the user interface would start
* playing the tones. We do that here because we may be harvesting
* STUN/TURN addresses in initiateSession() which would take a while.
*/
callPeer.setState(CallPeerState.CONNECTING);
// if initializing session fails, set peer to failed
boolean sessionInitiated = false;
try
{
callPeer.initiateSession(sessionInitiateExtensions);
sessionInitiated = true;
}
finally
{
// if initialization throws an exception
if (!sessionInitiated)
callPeer.setState(CallPeerState.FAILED);
}
return callPeer;
}
/**
* Updates the Jingle sessions for the <tt>CallPeer</tt>s of this
* <tt>Call</tt>, to reflect the current state of the the video contents of
* this <tt>Call</tt>. Sends a <tt>content-modify</tt>, <tt>content-add</tt>
* or <tt>content-remove</tt> message to each of the current
* <tt>CallPeer</tt>s.
*
* @throws OperationFailedException if a problem occurred during message
* generation or there was a network problem
*/
@Override
public void modifyVideoContent()
throws OperationFailedException
{
if (logger.isDebugEnabled())
logger.debug("Updating video content for " + this);
boolean change = false;
for (CallPeerJabberImpl peer : getCallPeerList())
change |= peer.sendModifyVideoContent();
if (change)
fireCallChangeEvent(
CallChangeEvent.CALL_PARTICIPANTS_CHANGE, null, null);
}
/**
* Notifies this instance that a specific <tt>ColibriConferenceIQ</tt> has
* been received.
*
* @param conferenceIQ the <tt>ColibriConferenceIQ</tt> which has been
* received
* @return <tt>true</tt> if the specified <tt>conferenceIQ</tt> was
* processed by this instance and no further processing is to be performed
* by other possible processors of <tt>ColibriConferenceIQ</tt>s; otherwise,
* <tt>false</tt>. Because a <tt>ColibriConferenceIQ</tt> request sent from
* the Jitsi Videobridge server to the application as its client concerns a
* specific <tt>CallJabberImpl</tt> implementation, no further processing by
* other <tt>CallJabberImpl</tt> instances is necessary once the
* <tt>ColibriConferenceIQ</tt> is processed by the associated
* <tt>CallJabberImpl</tt> instance.
*/
boolean processColibriConferenceIQ(ColibriConferenceIQ conferenceIQ)
{
if (colibri == null)
{
/*
* This instance has not set up any conference using the Jitsi
* Videobridge server-side technology yet so it cannot be bothered
* with related requests.
*/
return false;
}
else if (conferenceIQ.getID().equals(colibri.getID()))
{
/*
* Remove the local Channels (from the specified conferenceIQ) i.e.
* the Channels on which the local peer/user is sending to the Jitsi
* Videobridge server because they concern this Call only and not
* its CallPeers.
*/
for (MediaType mediaType : MediaType.values())
{
String contentName = mediaType.toString();
ColibriConferenceIQ.Content content
= conferenceIQ.getContent(contentName);
if (content != null)
{
ColibriConferenceIQ.Content thisContent
= colibri.getContent(contentName);
if ((thisContent != null)
&& (thisContent.getChannelCount() > 0))
{
ColibriConferenceIQ.Channel thisChannel
= thisContent.getChannel(0);
ColibriConferenceIQ.Channel channel
= content.getChannel(thisChannel.getID());
if (channel != null)
content.removeChannel(channel);
}
}
}
for (CallPeerJabberImpl callPeer : getCallPeerList())
callPeer.processColibriConferenceIQ(conferenceIQ);
/*
* We have removed the local Channels from the specified
* conferenceIQ. Consequently, it is no longer the same and fit for
* processing by other CallJabberImpl instances.
*/
return true;
}
else
{
/*
* This instance has set up a conference using the Jitsi Videobridge
* server-side technology but it is not the one referred to by the
* specified conferenceIQ i.e. the specified conferenceIQ does not
* concern this instance.
*/
return false;
}
}
/**
* Creates a new call peer and sends a RINGING response.
*
* @param jingleIQ the {@link JingleIQ} that created the session.
*
* @return the newly created {@link CallPeerJabberImpl} (the one that sent
* the INVITE).
*/
public CallPeerJabberImpl processSessionInitiate(JingleIQ jingleIQ)
{
// Use the IQs 'from', instead of the jingle 'initiator' field,
// because we want to make sure that following IQs are sent with the
// correct 'to'.
String remoteParty = jingleIQ.getFrom();
boolean autoAnswer = false;
CallPeerJabberImpl attendant = null;
OperationSetBasicTelephonyJabberImpl basicTelephony = null;
CallPeerJabberImpl callPeer
= new CallPeerJabberImpl(remoteParty, this, jingleIQ);
addCallPeer(callPeer);
/*
* We've already sent ack to the specified session-initiate so if it has
* been sent as part of an attended transfer, we have to hang up on the
* attendant.
*/
try
{
TransferPacketExtension transfer
= (TransferPacketExtension)
jingleIQ.getExtension(
TransferPacketExtension.ELEMENT_NAME,
TransferPacketExtension.NAMESPACE);
if (transfer != null)
{
String sid = transfer.getSID();
if (sid != null)
{
ProtocolProviderServiceJabberImpl protocolProvider
= getProtocolProvider();
basicTelephony
= (OperationSetBasicTelephonyJabberImpl)
protocolProvider.getOperationSet(
OperationSetBasicTelephony.class);
CallJabberImpl attendantCall
= basicTelephony
.getActiveCallsRepository()
.findSID(sid);
if (attendantCall != null)
{
attendant = attendantCall.getPeer(sid);
if ((attendant != null)
&& basicTelephony
.getFullCalleeURI(attendant.getAddress())
.equals(transfer.getFrom())
&& protocolProvider.getOurJID().equals(
transfer.getTo()))
{
//basicTelephony.hangupCallPeer(attendant);
autoAnswer = true;
}
}
}
}
}
catch (Throwable t)
{
logger.error(
"Failed to hang up on attendant"
+ " as part of session transfer",
t);
if (t instanceof ThreadDeath)
throw (ThreadDeath) t;
}
CoinPacketExtension coin
= (CoinPacketExtension)
jingleIQ.getExtension(
CoinPacketExtension.ELEMENT_NAME,
CoinPacketExtension.NAMESPACE);
if (coin != null)
{
boolean b
= Boolean.parseBoolean(
(String)
coin.getAttribute(
CoinPacketExtension.ISFOCUS_ATTR_NAME));
callPeer.setConferenceFocus(b);
}
//before notifying about this call, make sure that it looks alright
callPeer.processSessionInitiate(jingleIQ);
// if paranoia is set, to accept the call we need to know that
// the other party has support for media encryption
if (getProtocolProvider().getAccountID().getAccountPropertyBoolean(
ProtocolProviderFactory.MODE_PARANOIA, false)
&& callPeer.getMediaHandler().getAdvertisedEncryptionMethods()
.length
== 0)
{
//send an error response;
String reasonText
= JabberActivator.getResources().getI18NString(
"service.gui.security.encryption.required");
JingleIQ errResp
= JinglePacketFactory.createSessionTerminate(
jingleIQ.getTo(),
jingleIQ.getFrom(),
jingleIQ.getSID(),
Reason.SECURITY_ERROR,
reasonText);
callPeer.setState(CallPeerState.FAILED, reasonText);
getProtocolProvider().getConnection().sendPacket(errResp);
return null;
}
if (callPeer.getState() == CallPeerState.FAILED)
return null;
callPeer.setState( CallPeerState.INCOMING_CALL );
// in case of attended transfer, auto answer the call
if (autoAnswer)
{
/* answer directly */
try
{
callPeer.answer();
}
catch(Exception e)
{
logger.info(
"Exception occurred while answer transferred call",
e);
callPeer = null;
}
// hang up now
try
{
basicTelephony.hangupCallPeer(attendant);
}
catch(OperationFailedException e)
{
logger.error(
"Failed to hang up on attendant as part of session"
+ " transfer",
e);
}
return callPeer;
}
/* see if offer contains audio and video so that we can propose
* option to the user (i.e. answer with video if it is a video call...)
*/
List<ContentPacketExtension> offer
= callPeer.getSessionIQ().getContentList();
Map<MediaType, MediaDirection> directions
= new HashMap<MediaType, MediaDirection>();
directions.put(MediaType.AUDIO, MediaDirection.INACTIVE);
directions.put(MediaType.VIDEO, MediaDirection.INACTIVE);
for (ContentPacketExtension c : offer)
{
String contentName = c.getName();
MediaDirection remoteDirection
= JingleUtils.getDirection(c, callPeer.isInitiator());
if (MediaType.AUDIO.toString().equals(contentName))
directions.put(MediaType.AUDIO, remoteDirection);
else if (MediaType.VIDEO.toString().equals(contentName))
directions.put(MediaType.VIDEO, remoteDirection);
}
// If this was the first peer we added in this call, then the call is
// new and we need to notify everyone of its creation.
if (getCallPeerCount() == 1)
{
parentOpSet.fireCallEvent(
CallEvent.CALL_RECEIVED,
this,
directions);
}
// Manages auto answer with "audio only", or "audio/video" answer.
OperationSetAutoAnswerJabberImpl autoAnswerOpSet
= (OperationSetAutoAnswerJabberImpl)
getProtocolProvider().getOperationSet(
OperationSetBasicAutoAnswer.class);
if (autoAnswerOpSet != null)
autoAnswerOpSet.autoAnswer(this, directions);
return callPeer;
}
/**
* Updates the state of the local DTLS-SRTP endpoint (i.e. the local
* <tt>DtlsControl</tt> instance) from the state of the remote DTLS-SRTP
* endpoint represented by a specific <tt>ColibriConferenceIQ.Channel</tt>.
*
* @param peer the <tt>CallPeer</tt> associated with the method invocation
* @param channel the <tt>ColibriConferenceIQ.Channel</tt> which represents
* the state of the remote DTLS-SRTP endpoint
* @param mediaType the <tt>MediaType</tt> of the media to be transmitted
* over the DTLS-SRTP session
*/
private boolean addDtlsAdvertisedEncryptions(
CallPeerJabberImpl peer,
ColibriConferenceIQ.Channel channel,
MediaType mediaType)
{
CallPeerMediaHandlerJabberImpl peerMediaHandler
= peer.getMediaHandler();
DtlsControl dtlsControl
= (DtlsControl)
peerMediaHandler.getSrtpControls().get(
mediaType,
SrtpControlType.DTLS_SRTP);
if (dtlsControl != null)
{
dtlsControl.setSetup(
peer.isInitiator()
? DtlsControl.Setup.ACTIVE
: DtlsControl.Setup.PASSIVE);
}
IceUdpTransportPacketExtension remoteTransport = channel.getTransport();
return
peerMediaHandler.addDtlsAdvertisedEncryptions(
true,
remoteTransport,
mediaType);
}
/**
* Updates the state of the remote DTLS-SRTP endpoint represented by a
* specific <tt>ColibriConferenceIQ.Channel</tt> from the state of the local
* DTLS-SRTP endpoint. The specified <tt>channel</tt> is to be used by the
* conference focus for the purposes of transmitting media between a remote
* peer and the Jitsi Videobridge server.
*
* @param mediaType the <tt>MediaType</tt> of the media to be transmitted
* over the DTLS-SRTP session
* @param localContent the <tt>ContentPacketExtension</tt> of the local peer
* in the negotiation between the local and the remote peers. If
* <tt>remoteContent</tt> is <tt>null</tt>, represents an offer from the
* local peer to the remote peer; otherwise, represents an answer from the
* local peer to an offer from the remote peer.
* @param remoteContent the <tt>ContentPacketExtension</tt>, if any, of the
* remote peer in the negotiation between the local and the remote peers. If
* <tt>null</tt>, <tt>localContent</tt> represents an offer from the local
* peer to the remote peer; otherwise, <tt>localContent</tt> represents an
* answer from the local peer to an offer from the remote peer
* @param peer the <tt>CallPeer</tt> which represents the remote peer and
* which is associated with the specified <tt>channel</tt>
* @param channel the <tt>ColibriConferenceIQ.Channel</tt> which represents
* the state of the remote DTLS-SRTP endpoint.
*/
private void setDtlsEncryptionOnChannel(
MediaType mediaType,
ContentPacketExtension localContent,
ContentPacketExtension remoteContent,
CallPeerJabberImpl peer,
ColibriConferenceIQ.Channel channel)
{
AccountID accountID = getProtocolProvider().getAccountID();
if (accountID.getAccountPropertyBoolean(
ProtocolProviderFactory.DEFAULT_ENCRYPTION,
true)
&& accountID.isEncryptionProtocolEnabled(
SrtpControlType.DTLS_SRTP)
&& (remoteContent != null))
{
IceUdpTransportPacketExtension remoteTransport
= remoteContent.getFirstChildOfType(
IceUdpTransportPacketExtension.class);
if (remoteTransport != null)
{
List<DtlsFingerprintPacketExtension> remoteFingerprints
= remoteTransport.getChildExtensionsOfType(
DtlsFingerprintPacketExtension.class);
if (!remoteFingerprints.isEmpty())
{
IceUdpTransportPacketExtension localTransport
= ensureTransportOnChannel(channel, peer);
if (localTransport != null)
{
List<DtlsFingerprintPacketExtension> localFingerprints
= localTransport.getChildExtensionsOfType(
DtlsFingerprintPacketExtension.class);
if (localFingerprints.isEmpty())
{
for (DtlsFingerprintPacketExtension remoteFingerprint
: remoteFingerprints)
{
DtlsFingerprintPacketExtension localFingerprint
= new DtlsFingerprintPacketExtension();
localFingerprint.setFingerprint(
remoteFingerprint.getFingerprint());
localFingerprint.setHash(
remoteFingerprint.getHash());
localTransport.addChildExtension(
localFingerprint);
}
}
}
}
}
}
}
/**
* Updates the state of the remote DTLS-SRTP endpoint represented by a
* specific <tt>ColibriConferenceIQ.Channel</tt> from the state of the local
* DTLS-SRTP endpoint (i.e. the local <tt>DtlsControl</tt> instance). The
* specified <tt>channel</tt> is to be used by the conference focus for the
* purposes of transmitting media between the local peer and the Jitsi
* Videobridge server.
*
* @param jitsiVideobridge the address/JID of the Jitsi Videobridge
* @param peer the <tt>CallPeer</tt> associated with the method invocation
* @param mediaType the <tt>MediaType</tt> of the media to be transmitted
* over the DTLS-SRTP session
* @param channel the <tt>ColibriConferenceIQ.Channel</tt> which represents
* the state of the remote DTLS-SRTP endpoint.
*/
private void setDtlsEncryptionOnChannel(
String jitsiVideobridge,
CallPeerJabberImpl peer,
MediaType mediaType,
ColibriConferenceIQ.Channel channel)
{
ProtocolProviderServiceJabberImpl protocolProvider
= getProtocolProvider();
AccountID accountID = protocolProvider.getAccountID();
if (accountID.getAccountPropertyBoolean(
ProtocolProviderFactory.DEFAULT_ENCRYPTION,
true)
&& accountID.isEncryptionProtocolEnabled(
SrtpControlType.DTLS_SRTP)
&& protocolProvider.isFeatureSupported(
jitsiVideobridge,
ProtocolProviderServiceJabberImpl
.URN_XMPP_JINGLE_DTLS_SRTP))
{
CallPeerMediaHandlerJabberImpl mediaHandler
= peer.getMediaHandler();
DtlsControl dtlsControl
= (DtlsControl)
mediaHandler.getSrtpControls().getOrCreate(
mediaType,
SrtpControlType.DTLS_SRTP);
if (dtlsControl != null)
{
IceUdpTransportPacketExtension transport
= ensureTransportOnChannel(channel, peer);
if (transport != null)
setDtlsEncryptionOnTransport(dtlsControl, transport);
}
}
}
/**
* Sets the properties (i.e. fingerprint and hash function) of a specific
* <tt>DtlsControl</tt> on the specific
* <tt>IceUdpTransportPacketExtension</tt>.
*
* @param dtlsControl the <tt>DtlsControl</tt> the properties of which are
* to be set on the specified <tt>localTransport</tt>
* @param localTransport the <tt>IceUdpTransportPacketExtension</tt> on
* which the properties of the specified <tt>dtlsControl</tt> are to be set
*/
static void setDtlsEncryptionOnTransport(
DtlsControl dtlsControl,
IceUdpTransportPacketExtension localTransport)
{
String fingerprint = dtlsControl.getLocalFingerprint();
String hash = dtlsControl.getLocalFingerprintHashFunction();
DtlsFingerprintPacketExtension fingerprintPE
= localTransport.getFirstChildOfType(
DtlsFingerprintPacketExtension.class);
if (fingerprintPE == null)
{
fingerprintPE = new DtlsFingerprintPacketExtension();
localTransport.addChildExtension(fingerprintPE);
}
fingerprintPE.setFingerprint(fingerprint);
fingerprintPE.setHash(hash);
}
private void setTransportOnChannel(
CallPeerJabberImpl peer,
String media,
ColibriConferenceIQ.Channel channel)
throws OperationFailedException
{
PacketExtension transport
= peer.getMediaHandler().getTransportManager().createTransport(
media);
if (transport instanceof IceUdpTransportPacketExtension)
channel.setTransport((IceUdpTransportPacketExtension) transport);
}
private void setTransportOnChannel(
String media,
ContentPacketExtension localContent,
ContentPacketExtension remoteContent,
CallPeerJabberImpl peer,
ColibriConferenceIQ.Channel channel)
throws OperationFailedException
{
if (remoteContent != null)
{
IceUdpTransportPacketExtension transport
= remoteContent.getFirstChildOfType(
IceUdpTransportPacketExtension.class);
channel.setTransport(
TransportManagerJabberImpl.cloneTransportAndCandidates(
transport));
}
}
/**
* Makes an attempt to ensure that a specific
* <tt>ColibriConferenceIQ.Channel</tt> has a non-<tt>null</tt>
* <tt>transport</tt> set. If the specified <tt>channel</tt> does not have
* a <tt>transport</tt>, the method invokes the <tt>TransportManager</tt> of
* the specified <tt>CallPeerJabberImpl</tt> to initialize a new
* <tt>PacketExtension</tt>.
*
* @param channel the <tt>ColibriConferenceIQ.Channel</tt> to ensure the
* <tt>transport</tt> on
* @param peer the <tt>CallPeerJabberImpl</tt> which is associated with the
* specified <tt>channel</tt> and which specifies the
* <tt>TransportManager</tt> to be described in the specified
* <tt>channel</tt>
* @return the <tt>transport</tt> of the specified <tt>channel</tt>
*/
private IceUdpTransportPacketExtension ensureTransportOnChannel(
ColibriConferenceIQ.Channel channel,
CallPeerJabberImpl peer)
{
IceUdpTransportPacketExtension transport
= channel.getTransport();
if (transport == null)
{
PacketExtension pe
= peer
.getMediaHandler()
.getTransportManager()
.createTransportPacketExtension();
if (pe instanceof IceUdpTransportPacketExtension)
{
transport = (IceUdpTransportPacketExtension) pe;
channel.setTransport(transport);
}
}
return transport;
}
/**
* Gets the entity ID of the Jitsi Videobridge to be utilized by this
* <tt>Call</tt> for the purposes of establishing a server-assisted
* telephony conference.
*
* @return the entity ID of the Jitsi Videobridge to be utilized by this
* <tt>Call</tt> for the purposes of establishing a server-assisted
* telephony conference.
*/
public String getJitsiVideobridge()
{
if ((this.jitsiVideobridge == null)
&& getConference().isJitsiVideobridge())
{
String jitsiVideobridge
= getProtocolProvider().getJitsiVideobridge();
if (jitsiVideobridge != null)
this.jitsiVideobridge = jitsiVideobridge;
}
return this.jitsiVideobridge;
}
/**
* {@inheritDoc}
*
* Implements
* {@link net.java.sip.communicator.service.protocol.event.DTMFListener#toneReceived(net.java.sip.communicator.service.protocol.event.DTMFReceivedEvent)}
*
* Forwards DTMF events to the <tt>IncomingDTMF</tt> operation set, setting
* this <tt>Call</tt> as the source.
*/
@Override
public void toneReceived(DTMFReceivedEvent evt)
{
OperationSetIncomingDTMF opSet
= getProtocolProvider()
.getOperationSet(OperationSetIncomingDTMF.class);
if (opSet != null && opSet instanceof OperationSetIncomingDTMFJabberImpl)
{
// Re-fire the event using this Call as the source.
((OperationSetIncomingDTMFJabberImpl) opSet).toneReceived(
new DTMFReceivedEvent(
this,
evt.getValue(),
evt.getDuration(),
evt.getStart()));
}
}
}