/*
* 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.reflect.*;
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.extensions.jingle.ContentPacketExtension.SendersEnum;
import net.java.sip.communicator.impl.protocol.jabber.jinglesdp.*;
import net.java.sip.communicator.service.protocol.*;
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.smack.util.*;
import org.jivesoftware.smackx.packet.*;
/**
* Implements a Jabber CallPeer.
*
* @author Emil Ivov
* @author Lyubomir Marinov
* @author Boris Grozev
*/
public class CallPeerJabberImpl
extends AbstractCallPeerJabberGTalkImpl
{
/**
* The Logger used by the CallPeerJabberImpl class and its
* instances for logging output.
*/
private static final Logger logger
= Logger.getLogger(CallPeerJabberImpl.class);
/**
* If the call is cancelled before session-initiate is sent.
*/
private boolean cancelled = false;
/**
* Synchronization object for candidates available.
*/
private final Object candSyncRoot = new Object();
/**
* If the content-add does not contains candidates.
*/
private boolean contentAddWithNoCands = false;
/**
* If we have processed the session initiate.
*/
private boolean sessionInitiateProcessed = false;
/**
* Synchronization object. Synchronization object? Wow, who would have
* thought! ;) Would be great to have a word on what we are syncing with it
*/
private final Object sessionInitiateSyncRoot = new Object();
/**
* Synchronization object for SID.
*/
private final Object sidSyncRoot = new Object();
/**
* The current value of the 'senders' field of the audio content in the
* Jingle session with this CallPeer.
* null should be interpreted as 'both', which is the default in
* Jingle if the XML attribute is missing.
*/
private SendersEnum audioSenders = SendersEnum.none;
/**
* The current value of the 'senders' field of the video content in the
* Jingle session with this CallPeer.
* null should be interpreted as 'both', which is the default in
* Jingle if the XML attribute is missing.
*/
private SendersEnum videoSenders = SendersEnum.none;
/**
* Creates a new call peer with address peerAddress.
*
* @param peerAddress the Jabber address of the new call peer.
* @param owningCall the call that contains this call peer.
*/
public CallPeerJabberImpl(String peerAddress,
CallJabberImpl owningCall)
{
super(peerAddress, owningCall);
setMediaHandler(new CallPeerMediaHandlerJabberImpl(this));
}
/**
* Creates a new call peer with address peerAddress.
*
* @param peerAddress the Jabber address of the new call peer.
* @param owningCall the call that contains this call peer.
* @param sessionIQ The session-initiate JingleIQ which was
* received from peerAddress and caused the creation of this
* CallPeerJabberImpl
*/
public CallPeerJabberImpl(String peerAddress,
CallJabberImpl owningCall,
JingleIQ sessionIQ)
{
this(peerAddress, owningCall);
this.sessionInitIQ = sessionIQ;
}
/**
* Send a session-accept JingleIQ to this CallPeer
* @throws OperationFailedException if we fail to create or send the
* response.
*/
public synchronized void answer()
throws OperationFailedException
{
Iterable answer;
CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
try
{
mediaHandler
.getTransportManager()
.wrapupConnectivityEstablishment();
answer = mediaHandler.generateSessionAccept();
for (ContentPacketExtension c : answer)
setSenders(getMediaType(c), c.getSenders());
}
catch(Exception exc)
{
logger.info("Failed to answer an incoming call", exc);
//send an error response
String reasonText = "Error: " + exc.getMessage();
JingleIQ errResp
= JinglePacketFactory.createSessionTerminate(
sessionInitIQ.getTo(),
sessionInitIQ.getFrom(),
sessionInitIQ.getSID(),
Reason.FAILED_APPLICATION,
reasonText);
setState(CallPeerState.FAILED, reasonText);
getProtocolProvider().getConnection().sendPacket(errResp);
return;
}
JingleIQ response
= JinglePacketFactory.createSessionAccept(
sessionInitIQ.getTo(),
sessionInitIQ.getFrom(),
getSID(),
answer);
//send the packet first and start the stream later in case the media
//relay needs to see it before letting hole punching techniques through.
getProtocolProvider().getConnection().sendPacket(response);
try
{
mediaHandler.start();
}
catch(UndeclaredThrowableException e)
{
Throwable exc = e.getUndeclaredThrowable();
logger.info("Failed to establish a connection", exc);
//send an error response
String reasonText = "Error: " + exc.getMessage();
JingleIQ errResp
= JinglePacketFactory.createSessionTerminate(
sessionInitIQ.getTo(),
sessionInitIQ.getFrom(),
sessionInitIQ.getSID(),
Reason.GENERAL_ERROR,
reasonText);
setState(CallPeerState.FAILED, reasonText);
getProtocolProvider().getConnection().sendPacket(errResp);
return;
}
//tell everyone we are connected so that the audio notifications would
//stop
setState(CallPeerState.CONNECTED);
}
/**
* Returns the session ID of the Jingle session associated with this call.
*
* @return the session ID of the Jingle session associated with this call.
*/
@Override
public String getSID()
{
return sessionInitIQ != null ? sessionInitIQ.getSID() : null;
}
/**
* Returns the IQ ID of the Jingle session-initiate packet associated with
* this call.
*
* @return the IQ ID of the Jingle session-initiate packet associated with
* this call.
*/
public JingleIQ getSessionIQ()
{
return sessionInitIQ;
}
/**
* Ends the call with this CallPeer. Depending on the state
* of the peer the method would send a CANCEL, BYE, or BUSY_HERE message
* and set the new state to DISCONNECTED.
*
* @param failed indicates if the hangup is following to a call failure or
* simply a disconnect
* @param reasonText the text, if any, to be set on the
* ReasonPacketExtension as the value of its
* @param reasonOtherExtension the PacketExtension, if any, to be
* set on the ReasonPacketExtension as the value of its
* otherExtension property
*/
public void hangup(boolean failed,
String reasonText,
PacketExtension reasonOtherExtension)
{
CallPeerState prevPeerState = getState();
// do nothing if the call is already ended
if (CallPeerState.DISCONNECTED.equals(prevPeerState)
|| CallPeerState.FAILED.equals(prevPeerState))
{
if (logger.isDebugEnabled())
logger.debug("Ignoring a request to hangup a call peer "
+ "that is already DISCONNECTED");
return;
}
setState(
failed ? CallPeerState.FAILED : CallPeerState.DISCONNECTED,
reasonText);
JingleIQ responseIQ = null;
if (prevPeerState.equals(CallPeerState.CONNECTED)
|| CallPeerState.isOnHold(prevPeerState))
{
responseIQ = JinglePacketFactory.createBye(
getProtocolProvider().getOurJID(), peerJID, getSID());
}
else if (CallPeerState.CONNECTING.equals(prevPeerState)
|| CallPeerState.CONNECTING_WITH_EARLY_MEDIA.equals(prevPeerState)
|| CallPeerState.ALERTING_REMOTE_SIDE.equals(prevPeerState))
{
String jingleSID = getSID();
if(jingleSID == null)
{
synchronized(sidSyncRoot)
{
// we cancelled the call too early because the jingleSID
// is null (i.e. the session-initiate has not been created)
// and no need to send the session-terminate
cancelled = true;
return;
}
}
responseIQ = JinglePacketFactory.createCancel(
getProtocolProvider().getOurJID(), peerJID, getSID());
}
else if (prevPeerState.equals(CallPeerState.INCOMING_CALL))
{
responseIQ = JinglePacketFactory.createBusy(
getProtocolProvider().getOurJID(), peerJID, getSID());
}
else if (prevPeerState.equals(CallPeerState.BUSY)
|| prevPeerState.equals(CallPeerState.FAILED))
{
// For FAILED and BUSY we only need to update CALL_STATUS
// as everything else has been done already.
}
else
{
logger.info("Could not determine call peer state!");
}
if (responseIQ != null)
{
if (reasonOtherExtension != null)
{
ReasonPacketExtension reason
= (ReasonPacketExtension)
responseIQ.getExtension(
ReasonPacketExtension.ELEMENT_NAME,
ReasonPacketExtension.NAMESPACE);
if (reason != null)
{
reason.setOtherExtension(reasonOtherExtension);
}
else if(reasonOtherExtension instanceof ReasonPacketExtension)
{
responseIQ.setReason(
(ReasonPacketExtension)reasonOtherExtension);
}
}
getProtocolProvider().getConnection().sendPacket(responseIQ);
}
}
/**
* Creates and sends a session-initiate {@link JingleIQ}.
*
* @param sessionInitiateExtensions a collection of additional and optional
* PacketExtensions to be added to the session-initiate
* {@link JingleIQ} which is to initiate the session with this
* CallPeerJabberImpl
* @throws OperationFailedException exception
*/
protected synchronized void initiateSession(
Iterable sessionInitiateExtensions)
throws OperationFailedException
{
initiator = false;
//Create the media description that we'd like to send to the other side.
List offer
= getMediaHandler().createContentList();
ProtocolProviderServiceJabberImpl protocolProvider
= getProtocolProvider();
synchronized(sidSyncRoot)
{
sessionInitIQ
= JinglePacketFactory.createSessionInitiate(
protocolProvider.getOurJID(),
this.peerJID,
JingleIQ.generateSID(),
offer);
if(cancelled)
{
// we cancelled the call too early so no need to send the
// session-initiate to peer
getMediaHandler().getTransportManager().close();
return;
}
}
if (sessionInitiateExtensions != null)
{
for (PacketExtension sessionInitiateExtension
: sessionInitiateExtensions)
{
sessionInitIQ.addExtension(sessionInitiateExtension);
}
}
protocolProvider.getConnection().sendPacket(sessionInitIQ);
}
/**
* Notifies this instance that a specific ColibriConferenceIQ has
* been received. This CallPeerJabberImpl uses the part of the
* information provided in the specified conferenceIQ which
* concerns it only.
*
* @param conferenceIQ the ColibriConferenceIQ which has been
* received
*/
void processColibriConferenceIQ(ColibriConferenceIQ conferenceIQ)
{
/*
* CallPeerJabberImpl does not itself/directly know the specifics
* related to the channels allocated on the Jitsi Videobridge server.
* The channels contain transport and media-related information so
* forward the notification to CallPeerMediaHandlerJabberImpl.
*/
getMediaHandler().processColibriConferenceIQ(conferenceIQ);
}
/**
* Processes the content-accept {@link JingleIQ}.
*
* @param content The {@link JingleIQ} that contains content that remote
* peer has accepted
*/
public void processContentAccept(JingleIQ content)
{
List contents = content.getContentList();
CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
try
{
mediaHandler
.getTransportManager()
.wrapupConnectivityEstablishment();
mediaHandler.processAnswer(contents);
for (ContentPacketExtension c : contents)
setSenders(getMediaType(c), c.getSenders());
}
catch (Exception e)
{
logger.warn("Failed to process a content-accept", e);
// Send an error response.
String reason = "Error: " + e.getMessage();
JingleIQ errResp
= JinglePacketFactory.createSessionTerminate(
getProtocolProvider().getOurJID(),
peerJID,
sessionInitIQ.getSID(),
Reason.INCOMPATIBLE_PARAMETERS,
reason);
setState(CallPeerState.FAILED, reason);
getProtocolProvider().getConnection().sendPacket(errResp);
return;
}
mediaHandler.start();
}
/**
* Processes the content-add {@link JingleIQ}.
*
* @param content The {@link JingleIQ} that contains content that remote
* peer wants to be added
*/
public void processContentAdd(final JingleIQ content)
{
CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
List contents = content.getContentList();
Iterable answerContents;
JingleIQ contentIQ;
boolean noCands = false;
MediaStream oldVideoStream = mediaHandler.getStream(MediaType.VIDEO);
if(logger.isInfoEnabled())
logger.info("Looking for candidates in content-add.");
try
{
if(!contentAddWithNoCands)
{
mediaHandler.processOffer(contents);
/*
* Gingle transport will not put candidate in session-initiate
* and content-add.
*/
for(ContentPacketExtension c : contents)
{
if(JingleUtils.getFirstCandidate(c, 1) == null)
{
contentAddWithNoCands = true;
noCands = true;
}
}
}
// if no candidates are present, launch a new Thread which will
// process and wait for the connectivity establishment (otherwise
// the existing thread will be blocked and thus cannot receive
// transport-info with candidates
if(noCands)
{
new Thread()
{
@Override
public void run()
{
try
{
synchronized(candSyncRoot)
{
candSyncRoot.wait();
}
}
catch(InterruptedException e)
{
}
processContentAdd(content);
contentAddWithNoCands = false;
}
}.start();
if(logger.isInfoEnabled())
logger.info("No candidates found in content-add, started "
+ "new thread.");
return;
}
mediaHandler
.getTransportManager()
.wrapupConnectivityEstablishment();
if(logger.isInfoEnabled())
logger.info("Wrapping up connectivity establishment");
answerContents = mediaHandler.generateSessionAccept();
contentIQ = null;
}
catch(Exception e)
{
logger.warn("Exception occurred", e);
answerContents = null;
contentIQ
= JinglePacketFactory.createContentReject(
getProtocolProvider().getOurJID(),
this.peerJID,
getSID(),
answerContents);
}
if(contentIQ == null)
{
/* send content-accept */
contentIQ
= JinglePacketFactory.createContentAccept(
getProtocolProvider().getOurJID(),
this.peerJID,
getSID(),
answerContents);
for (ContentPacketExtension c : answerContents)
setSenders(getMediaType(c), c.getSenders());
}
getProtocolProvider().getConnection().sendPacket(contentIQ);
mediaHandler.start();
/*
* If a remote peer turns her video on in a conference which is hosted
* by the local peer and the local peer is not streaming her local
* video, reinvite the other remote peers to enable RTP translation.
*/
if (oldVideoStream == null)
{
MediaStream newVideoStream
= mediaHandler.getStream(MediaType.VIDEO);
if ((newVideoStream != null)
&& mediaHandler.isRTPTranslationEnabled(MediaType.VIDEO))
{
try
{
getCall().modifyVideoContent();
}
catch (OperationFailedException ofe)
{
logger.error("Failed to enable RTP translation", ofe);
}
}
}
}
/**
* Processes the content-modify {@link JingleIQ}.
*
* @param content The {@link JingleIQ} that contains content that remote
* peer wants to be modified
*/
public void processContentModify(JingleIQ content)
{
ContentPacketExtension ext = content.getContentList().get(0);
MediaType mediaType = getMediaType(ext);
try
{
boolean modify
= (ext.getFirstChildOfType(RtpDescriptionPacketExtension.class)
!= null);
getMediaHandler().reinitContent(ext.getName(), ext, modify);
setSenders(mediaType, ext.getSenders());
if (MediaType.VIDEO.equals(mediaType))
getCall().modifyVideoContent();
}
catch(Exception e)
{
logger.info("Failed to process an incoming content-modify", e);
// Send an error response.
String reason = "Error: " + e.getMessage();
JingleIQ errResp
= JinglePacketFactory.createSessionTerminate(
getProtocolProvider().getOurJID(),
peerJID,
sessionInitIQ.getSID(),
Reason.INCOMPATIBLE_PARAMETERS,
reason);
setState(CallPeerState.FAILED, reason);
getProtocolProvider().getConnection().sendPacket(errResp);
return;
}
}
/**
* Processes the content-reject {@link JingleIQ}.
*
* @param content The {@link JingleIQ}
*/
public void processContentReject(JingleIQ content)
{
if(content.getContentList().isEmpty())
{
//send an error response;
JingleIQ errResp = JinglePacketFactory.createSessionTerminate(
sessionInitIQ.getTo(), sessionInitIQ.getFrom(),
sessionInitIQ.getSID(), Reason.INCOMPATIBLE_PARAMETERS,
"Error: content rejected");
setState(CallPeerState.FAILED, "Error: content rejected");
getProtocolProvider().getConnection().sendPacket(errResp);
return;
}
}
/**
* Processes the content-remove {@link JingleIQ}.
*
* @param content The {@link JingleIQ} that contains content that remote
* peer wants to be removed
*/
public void processContentRemove(JingleIQ content)
{
List contents = content.getContentList();
boolean videoContentRemoved = false;
if (!contents.isEmpty())
{
CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
for(ContentPacketExtension c : contents)
{
mediaHandler.removeContent(c.getName());
MediaType mediaType = getMediaType(c);
setSenders(mediaType, SendersEnum.none);
if (MediaType.VIDEO.equals(mediaType))
videoContentRemoved = true;
}
/*
* TODO XEP-0166: Jingle says: If the content-remove results in zero
* content definitions for the session, the entity that receives the
* content-remove SHOULD send a session-terminate action to the
* other party (since a session with no content definitions is
* void).
*/
}
if (videoContentRemoved)
{
// removing of the video content might affect the other sessions
// in the call
try
{
getCall().modifyVideoContent();
}
catch (Exception e)
{
logger.warn("Failed to update Jingle sessions");
}
}
}
/**
* Processes a session-accept {@link JingleIQ}.
*
* @param sessionInitIQ The session-accept {@link JingleIQ} to process.
*/
public void processSessionAccept(JingleIQ sessionInitIQ)
{
this.sessionInitIQ = sessionInitIQ;
CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
List answer = sessionInitIQ.getContentList();
try
{
mediaHandler
.getTransportManager()
.wrapupConnectivityEstablishment();
mediaHandler.processAnswer(answer);
for (ContentPacketExtension c : answer)
setSenders(getMediaType(c), c.getSenders());
}
catch(Exception exc)
{
if (logger.isInfoEnabled())
logger.info("Failed to process a session-accept", exc);
//send an error response;
JingleIQ errResp = JinglePacketFactory.createSessionTerminate(
sessionInitIQ.getTo(), sessionInitIQ.getFrom(),
sessionInitIQ.getSID(), Reason.INCOMPATIBLE_PARAMETERS,
exc.getClass().getName() + ": " + exc.getMessage());
setState(CallPeerState.FAILED, "Error: " + exc.getMessage());
getProtocolProvider().getConnection().sendPacket(errResp);
return;
}
//tell everyone we are connected so that the audio notifications would
//stop
setState(CallPeerState.CONNECTED);
mediaHandler.start();
/*
* If video was added to the call after we sent the session-initiate
* to this peer, it needs to be added to this peer's session with a
* content-add.
*/
sendModifyVideoContent();
}
/**
* Handles the specified session info packet according to its
* content.
*
* @param info the {@link SessionInfoPacketExtension} that we just received.
*/
public void processSessionInfo(SessionInfoPacketExtension info)
{
switch (info.getType())
{
case ringing:
setState(CallPeerState.ALERTING_REMOTE_SIDE);
break;
case hold:
getMediaHandler().setRemotelyOnHold(true);
reevalRemoteHoldStatus();
break;
case unhold:
case active:
getMediaHandler().setRemotelyOnHold(false);
reevalRemoteHoldStatus();
break;
default:
logger.warn("Received SessionInfoPacketExtension of unknown type");
}
}
/**
* Processes the session initiation {@link JingleIQ} that we were created
* with, passing its content to the media handler and then sends either a
* "session-info/ringing" or a "session-terminate" response.
*
* @param sessionInitIQ The {@link JingleIQ} that created the session that
* we are handling here.
*/
protected synchronized void processSessionInitiate(JingleIQ sessionInitIQ)
{
// Do initiate the session.
this.sessionInitIQ = sessionInitIQ;
this.initiator = true;
// This is the SDP offer that came from the initial session-initiate.
// Contrary to SIP, we are guaranteed to have content because XEP-0166
// says: "A session consists of at least one content type at a time."
List offer = sessionInitIQ.getContentList();
try
{
getMediaHandler().processOffer(offer);
CoinPacketExtension coin = null;
for(PacketExtension ext : sessionInitIQ.getExtensions())
{
if(ext.getElementName().equals(
CoinPacketExtension.ELEMENT_NAME))
{
coin = (CoinPacketExtension)ext;
break;
}
}
/* does the call peer acts as a conference focus ? */
if(coin != null)
{
setConferenceFocus(Boolean.parseBoolean(
(String)coin.getAttribute("isfocus")));
}
}
catch(Exception ex)
{
logger.info("Failed to process an incoming session initiate", ex);
//send an error response;
String reasonText = "Error: " + ex.getMessage();
JingleIQ errResp
= JinglePacketFactory.createSessionTerminate(
sessionInitIQ.getTo(),
sessionInitIQ.getFrom(),
sessionInitIQ.getSID(),
Reason.INCOMPATIBLE_PARAMETERS,
reasonText);
setState(CallPeerState.FAILED, reasonText);
getProtocolProvider().getConnection().sendPacket(errResp);
return;
}
// If we do not get the info about the remote peer yet. Get it right
// now.
if(this.getDiscoveryInfo() == null)
{
String calleeURI = sessionInitIQ.getFrom();
retrieveDiscoveryInfo(calleeURI);
}
//send a ringing response
if (logger.isTraceEnabled())
logger.trace("will send ringing response: ");
getProtocolProvider().getConnection().sendPacket(
JinglePacketFactory.createRinging(sessionInitIQ));
synchronized(sessionInitiateSyncRoot)
{
sessionInitiateProcessed = true;
sessionInitiateSyncRoot.notify();
}
//if this is a 3264 initiator, let's give them an early peek at our
//answer so that they could start ICE (SIP-2-Jingle gateways won't
//be able to send their candidates unless they have this)
DiscoverInfo discoverInfo = getDiscoveryInfo();
if ((discoverInfo != null)
&& discoverInfo.containsFeature(
ProtocolProviderServiceJabberImpl.URN_IETF_RFC_3264))
{
getProtocolProvider().getConnection().sendPacket(
JinglePacketFactory.createDescriptionInfo(
sessionInitIQ.getTo(),
sessionInitIQ.getFrom(),
sessionInitIQ.getSID(),
getMediaHandler().getLocalContentList()));
}
}
/**
* Puts this peer into a {@link CallPeerState#DISCONNECTED}, indicating a
* reason to the user, if there is one.
*
* @param jingleIQ the {@link JingleIQ} that's terminating our session.
*/
public void processSessionTerminate(JingleIQ jingleIQ)
{
String reasonStr = "Call ended by remote side.";
ReasonPacketExtension reasonExt = jingleIQ.getReason();
if(reasonExt != null)
{
Reason reason = reasonExt.getReason();
if(reason != null)
reasonStr += " Reason: " + reason.toString() + ".";
String text = reasonExt.getText();
if(text != null)
reasonStr += " " + text;
}
setState(CallPeerState.DISCONNECTED, reasonStr);
}
/**
* Processes a specific "XEP-0251: Jingle Session Transfer"
* transfer packet (extension).
*
* @param transfer the "XEP-0251: Jingle Session Transfer" transfer packet
* (extension) to process
* @throws OperationFailedException if anything goes wrong while processing
* the specified transfer packet (extension)
*/
public void processTransfer(TransferPacketExtension transfer)
throws OperationFailedException
{
String attendantAddress = transfer.getFrom();
if (attendantAddress == null)
{
throw new OperationFailedException(
"Session transfer must contain a \'from\' attribute value.",
OperationFailedException.ILLEGAL_ARGUMENT);
}
String calleeAddress = transfer.getTo();
if (calleeAddress == null)
{
throw new OperationFailedException(
"Session transfer must contain a \'to\' attribute value.",
OperationFailedException.ILLEGAL_ARGUMENT);
}
// Checks if the transfer remote peer is contained by the roster of this
// account.
Roster roster = getProtocolProvider().getConnection().getRoster();
if(!roster.contains(StringUtils.parseBareAddress(calleeAddress)))
{
String failedMessage =
"Transfer impossible:\n"
+ "Account roster does not contain transfer peer: "
+ StringUtils.parseBareAddress(calleeAddress);
setState(CallPeerState.FAILED, failedMessage);
logger.info(failedMessage);
}
OperationSetBasicTelephonyJabberImpl basicTelephony
= (OperationSetBasicTelephonyJabberImpl)
getProtocolProvider()
.getOperationSet(OperationSetBasicTelephony.class);
CallJabberImpl calleeCall = new CallJabberImpl(basicTelephony);
TransferPacketExtension calleeTransfer = new TransferPacketExtension();
String sid = transfer.getSID();
calleeTransfer.setFrom(attendantAddress);
if (sid != null)
{
calleeTransfer.setSID(sid);
calleeTransfer.setTo(calleeAddress);
}
basicTelephony.createOutgoingCall(
calleeCall,
calleeAddress,
Arrays.asList(new PacketExtension[] { calleeTransfer }));
}
/**
* Processes the transport-info {@link JingleIQ}.
*
* @param jingleIQ the transport-info {@link JingleIQ} to process
*/
public void processTransportInfo(JingleIQ jingleIQ)
{
/*
* The transport-info action is used to exchange transport candidates so
* it only concerns the mediaHandler.
*/
try
{
if(isInitiator())
{
synchronized(sessionInitiateSyncRoot)
{
if(!sessionInitiateProcessed)
{
try
{
sessionInitiateSyncRoot.wait();
}
catch(InterruptedException e)
{
}
}
}
}
getMediaHandler().processTransportInfo(
jingleIQ.getContentList());
}
catch (OperationFailedException ofe)
{
logger.warn("Failed to process an incoming transport-info", ofe);
//send an error response
String reasonText = "Error: " + ofe.getMessage();
JingleIQ errResp
= JinglePacketFactory.createSessionTerminate(
getProtocolProvider().getOurJID(),
peerJID,
sessionInitIQ.getSID(),
Reason.GENERAL_ERROR,
reasonText);
setState(CallPeerState.FAILED, reasonText);
getProtocolProvider().getConnection().sendPacket(errResp);
return;
}
synchronized(candSyncRoot)
{
candSyncRoot.notify();
}
}
/**
* Puts the CallPeer represented by this instance on or off hold.
*
* @param onHold true to have the CallPeer put on hold;
* false, otherwise
*
* @throws OperationFailedException if we fail to construct or send the
* INVITE request putting the remote side on/off hold.
*/
public void putOnHold(boolean onHold)
throws OperationFailedException
{
CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
mediaHandler.setLocallyOnHold(onHold);
SessionInfoType type;
if(onHold)
type = SessionInfoType.hold;
else
{
type = SessionInfoType.unhold;
getMediaHandler().reinitAllContents();
}
//we are now on hold and need to realize this before potentially
//spoiling it all with an exception while sending the packet :).
reevalLocalHoldStatus();
JingleIQ onHoldIQ = JinglePacketFactory.createSessionInfo(
getProtocolProvider().getOurJID(),
peerJID,
getSID(),
type);
getProtocolProvider().getConnection().sendPacket(onHoldIQ);
}
/**
* Send a content-add to add video setup.
*/
private void sendAddVideoContent()
{
List contents;
try
{
contents = getMediaHandler().createContentList(MediaType.VIDEO);
}
catch(Exception exc)
{
logger.warn("Failed to gather content for video type", exc);
return;
}
ProtocolProviderServiceJabberImpl protocolProvider
= getProtocolProvider();
JingleIQ contentIQ
= JinglePacketFactory.createContentAdd(
protocolProvider.getOurJID(),
this.peerJID,
getSID(),
contents);
protocolProvider.getConnection().sendPacket(contentIQ);
}
/**
* Sends a content message to reflect changes in the setup such as
* the local peer/user becoming a conference focus.
*/
public void sendCoinSessionInfo()
{
JingleIQ sessionInfoIQ
= JinglePacketFactory.createSessionInfo(
getProtocolProvider().getOurJID(),
this.peerJID,
getSID());
CoinPacketExtension coinExt
= new CoinPacketExtension(getCall().isConferenceFocus());
sessionInfoIQ.addExtension(coinExt);
getProtocolProvider().getConnection().sendPacket(sessionInfoIQ);
}
/**
* Returns the MediaDirection that should be set for the content
* of type mediaType in the Jingle session for this
* CallPeer.
* If we are the focus of a conference and are doing RTP translation,
* takes into account the other CallPeers in the Call.
*
* @param mediaType the MediaType for which to return the
* MediaDirection
* @return the MediaDirection that should be used for the content
* of type mediaType in the Jingle session for this
* CallPeer.
*/
private MediaDirection getDirectionForJingle(MediaType mediaType)
{
MediaDirection direction = MediaDirection.INACTIVE;
CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
// If we are streaming media, the direction should allow sending
if ( (MediaType.AUDIO == mediaType &&
mediaHandler.isLocalAudioTransmissionEnabled()) ||
(MediaType.VIDEO == mediaType &&
isLocalVideoStreaming()))
direction = direction.or(MediaDirection.SENDONLY);
// If we are receiving media from this CallPeer, the direction should
// allow receiving
SendersEnum senders = getSenders(mediaType);
if (senders == null || senders == SendersEnum.both ||
(isInitiator() && senders == SendersEnum.initiator) ||
(!isInitiator() && senders == SendersEnum.responder))
direction = direction.or(MediaDirection.RECVONLY);
// If we are the focus of a conference and we are receiving media from
// another CallPeer in the same Call, the direction should allow sending
CallJabberImpl call = getCall();
if (call != null && call.isConferenceFocus())
{
for (CallPeerJabberImpl peer : call.getCallPeerList())
{
if (peer != this)
{
senders = peer.getSenders(mediaType);
if (senders == null || senders == SendersEnum.both ||
(peer.isInitiator()
&& senders == SendersEnum.initiator) ||
(!peer.isInitiator()
&& senders == SendersEnum.responder))
{
direction = direction.or(MediaDirection.SENDONLY);
break;
}
}
}
}
return direction;
}
/**
* Send, if necessary, a jingle content message to reflect change
* in video setup. Whether the jingle session should have a video content,
* and if so, the value of the senders field is determined
* based on whether we are streaming local video and, if we are the focus
* of a conference, on the other peers in the conference.
* The message can be content-modify if video content exists (and the
* senders field changes), content-add or content-remove.
*
* @return true if a jingle content message was sent.
*/
public boolean sendModifyVideoContent()
{
CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
MediaDirection direction = getDirectionForJingle(MediaType.VIDEO);
ContentPacketExtension remoteContent
= mediaHandler.getLocalContent(MediaType.VIDEO.toString());
if (remoteContent == null)
{
if (direction == MediaDirection.INACTIVE)
{
// no video content, none needed
return false;
}
else
{
if (getState() == CallPeerState.CONNECTED)
{
if (logger.isInfoEnabled())
logger.info("Adding video content for " + this);
sendAddVideoContent();
return true;
}
return false;
}
}
else
{
if (direction == MediaDirection.INACTIVE)
{
sendRemoveVideoContent();
return true;
}
}
SendersEnum senders = getSenders(MediaType.VIDEO);
if (senders == null)
senders = SendersEnum.both;
SendersEnum newSenders = SendersEnum.none;
if (MediaDirection.SENDRECV == direction)
newSenders = SendersEnum.both;
else if (MediaDirection.RECVONLY == direction)
newSenders = isInitiator()
? SendersEnum.initiator : SendersEnum.responder;
else if (MediaDirection.SENDONLY == direction)
newSenders = isInitiator()
? SendersEnum.responder : SendersEnum.initiator;
/*
* Send Content-Modify
*/
ContentPacketExtension ext = new ContentPacketExtension();
String remoteContentName = remoteContent.getName();
ext.setSenders(newSenders);
ext.setCreator(remoteContent.getCreator());
ext.setName(remoteContentName);
if (newSenders != senders)
{
if (logger.isInfoEnabled())
logger.info("Sending content modify, senders: "
+ senders + "->" + newSenders);
ProtocolProviderServiceJabberImpl protocolProvider
= getProtocolProvider();
JingleIQ contentIQ
= JinglePacketFactory.createContentModify(
protocolProvider.getOurJID(),
this.peerJID,
getSID(),
ext);
protocolProvider.getConnection().sendPacket(contentIQ);
}
try
{
mediaHandler.reinitContent(remoteContentName, ext, false);
mediaHandler.start();
}
catch(Exception e)
{
logger.warn("Exception occurred during media reinitialization", e);
}
return (newSenders != senders);
}
/**
* Send a content message to reflect change in video setup (start
* or stop).
*/
public void sendModifyVideoResolutionContent()
{
CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
ContentPacketExtension remoteContent
= mediaHandler.getRemoteContent(MediaType.VIDEO.toString());
ContentPacketExtension content;
logger.info("send modify-content to change resolution");
// send content-modify with RTP description
// create content list with resolution
try
{
content = mediaHandler.createContentForMedia(MediaType.VIDEO);
}
catch (Exception e)
{
logger.warn("Failed to gather content for video type", e);
return;
}
// if we are only receiving video senders is null
SendersEnum senders = remoteContent.getSenders();
if (senders != null)
content.setSenders(senders);
ProtocolProviderServiceJabberImpl protocolProvider
= getProtocolProvider();
JingleIQ contentIQ
= JinglePacketFactory.createContentModify(
protocolProvider.getOurJID(),
this.peerJID,
getSID(),
content);
protocolProvider.getConnection().sendPacket(contentIQ);
try
{
mediaHandler.reinitContent(remoteContent.getName(), content, false);
mediaHandler.start();
}
catch(Exception e)
{
logger.warn("Exception occurred when media reinitialization", e);
}
}
/**
* Send a content-remove to remove video setup.
*/
private void sendRemoveVideoContent()
{
CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
ContentPacketExtension content = new ContentPacketExtension();
ContentPacketExtension remoteContent
= mediaHandler.getRemoteContent(MediaType.VIDEO.toString());
if (remoteContent == null)
return;
String remoteContentName = remoteContent.getName();
content.setName(remoteContentName);
content.setCreator(remoteContent.getCreator());
content.setSenders(remoteContent.getSenders());
ProtocolProviderServiceJabberImpl protocolProvider
= getProtocolProvider();
JingleIQ contentIQ
= JinglePacketFactory.createContentRemove(
protocolProvider.getOurJID(),
this.peerJID,
getSID(),
Arrays.asList(content));
protocolProvider.getConnection().sendPacket(contentIQ);
mediaHandler.removeContent(remoteContentName);
setSenders(MediaType.VIDEO, SendersEnum.none);
}
/**
* Sends local candidate addresses from the local peer to the remote peer
* using the transport-info {@link JingleIQ}.
*
* @param contents the local candidate addresses to be sent from the local
* peer to the remote peer using the transport-info
* {@link JingleIQ}
*/
protected void sendTransportInfo(Iterable contents)
{
// if the call is canceled, do not start sending candidates in
// transport-info
if(cancelled)
return;
JingleIQ transportInfo = new JingleIQ();
for (ContentPacketExtension content : contents)
transportInfo.addContent(content);
ProtocolProviderServiceJabberImpl protocolProvider
= getProtocolProvider();
transportInfo.setAction(JingleAction.TRANSPORT_INFO);
transportInfo.setFrom(protocolProvider.getOurJID());
transportInfo.setSID(getSID());
transportInfo.setTo(getAddress());
transportInfo.setType(IQ.Type.SET);
PacketCollector collector
= protocolProvider.getConnection().createPacketCollector(
new PacketIDFilter(transportInfo.getPacketID()));
protocolProvider.getConnection().sendPacket(transportInfo);
collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
collector.cancel();
}
@Override
public void setState(CallPeerState newState, String reason, int reasonCode)
{
CallPeerState oldState = getState();
try
{
/*
* We need to dispose of the transport manager before the
* 'call' field is set to null, because if Jitsi Videobridge is in
* use, it (the call) is needed in order to expire the
* Videobridge channels.
*/
if (CallPeerState.DISCONNECTED.equals(newState)
|| CallPeerState.FAILED.equals(newState))
getMediaHandler().getTransportManager().close();
}
finally
{
super.setState(newState, reason, reasonCode);
}
if (CallPeerState.isOnHold(oldState)
&& CallPeerState.CONNECTED.equals(newState))
{
try
{
getCall().modifyVideoContent();
}
catch (OperationFailedException ofe)
{
logger.error("Failed to update call video state after " +
"'hold' status removed for "+this);
}
}
}
/**
* Transfer (in the sense of call transfer) this CallPeer to a
* specific callee address which may optionally be participating in an
* active Call.
*
* @param to the address of the callee to transfer this CallPeer to
* @param sid the Jingle session ID of the active Call between the
* local peer and the callee in the case of attended transfer; null
* in the case of unattended transfer
* @throws OperationFailedException if something goes wrong
*/
protected void transfer(String to, String sid)
throws OperationFailedException
{
JingleIQ transferSessionInfo = new JingleIQ();
ProtocolProviderServiceJabberImpl protocolProvider
= getProtocolProvider();
transferSessionInfo.setAction(JingleAction.SESSION_INFO);
transferSessionInfo.setFrom(protocolProvider.getOurJID());
transferSessionInfo.setSID(getSID());
transferSessionInfo.setTo(getAddress());
transferSessionInfo.setType(IQ.Type.SET);
TransferPacketExtension transfer = new TransferPacketExtension();
// Attended transfer.
if (sid != null)
{
/*
* Not really sure what the value of the "from" attribute of the
* "transfer" element should be but the examples in "XEP-0251:
* Jingle Session Transfer" has it in the case of attended transfer.
*/
transfer.setFrom(protocolProvider.getOurJID());
transfer.setSID(sid);
// Puts on hold the 2 calls before making the attended transfer.
OperationSetBasicTelephonyJabberImpl basicTelephony
= (OperationSetBasicTelephonyJabberImpl)
protocolProvider.getOperationSet(
OperationSetBasicTelephony.class);
CallPeerJabberImpl callPeer = basicTelephony.getActiveCallPeer(sid);
if(callPeer != null)
{
if(!CallPeerState.isOnHold(callPeer.getState()))
{
callPeer.putOnHold(true);
}
}
if(!CallPeerState.isOnHold(this.getState()))
{
this.putOnHold(true);
}
}
transfer.setTo(to);
transferSessionInfo.addExtension(transfer);
Connection connection = protocolProvider.getConnection();
PacketCollector collector = connection.createPacketCollector(
new PacketIDFilter(transferSessionInfo.getPacketID()));
protocolProvider.getConnection().sendPacket(transferSessionInfo);
Packet result
= collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
if(result == null)
{
// Log the failed transfer call and notify the user.
throw new OperationFailedException(
"No response to the \"transfer\" request.",
OperationFailedException.ILLEGAL_ARGUMENT);
}
else if (((IQ) result).getType() != IQ.Type.RESULT)
{
// Log the failed transfer call and notify the user.
throw new OperationFailedException(
"Remote peer does not manage call \"transfer\"."
+ "Response to the \"transfer\" request is: "
+ ((IQ) result).getType(),
OperationFailedException.ILLEGAL_ARGUMENT);
}
else
{
String message = ((sid == null) ? "Unattended" : "Attended")
+ " transfer to: "
+ to;
// Implements the SIP behavior: once the transfer is accepted, the
// current call is closed.
hangup(
false,
message,
new ReasonPacketExtension(Reason.SUCCESS,
message,
new TransferredPacketExtension()));
}
}
/**
* {@inheritDoc}
*/
public String getEntity()
{
return getAddress();
}
/**
* {@inheritDoc}
*
* In Jingle there isn't an actual "direction" parameter. We use the
* senders field to calculate the direction.
*/
@Override
public MediaDirection getDirection(MediaType mediaType)
{
SendersEnum senders = getSenders(mediaType);
if (senders == SendersEnum.none)
{
return MediaDirection.INACTIVE;
}
else if (senders == null || senders == SendersEnum.both)
{
return MediaDirection.SENDRECV;
}
else if (senders == SendersEnum.initiator)
{
return
isInitiator()
? MediaDirection.RECVONLY
: MediaDirection.SENDONLY;
}
else //senders == SendersEnum.responder
{
return
isInitiator()
? MediaDirection.SENDONLY
: MediaDirection.RECVONLY;
}
}
/**
* Gets the current value of the senders field of the content with
* name mediaType in the Jingle session with this
* CallPeer.
*
* @param mediaType the MediaType for which to get the current
* value of the senders field.
* @return the current value of the senders field of the content
* with name mediaType in the Jingle session with this
* CallPeer.
*/
public SendersEnum getSenders(MediaType mediaType)
{
switch (mediaType)
{
case AUDIO:
return audioSenders;
case VIDEO:
return videoSenders;
default:
return SendersEnum.none;
}
}
/**
* Set the current value of the senders field of the content with
* name mediaType in the Jingle session with this CallPeer
* @param mediaType the MediaType for which to get the current
* value of the senders field.
* @param senders the value to set
*/
public void setSenders(MediaType mediaType, SendersEnum senders)
{
switch(mediaType)
{
case AUDIO:
this.audioSenders = senders;
break;
case VIDEO:
this.videoSenders = senders;
break;
default:
throw new IllegalArgumentException("mediaType");
}
}
/**
* Gets the MediaType of content. If content
* does not have a description child and therefore not
* MediaType can be associated with it, tries to take the
* MediaType from the session's already established contents with
* the same name as content
* @param content the ContentPacketExtention for which to get the
* MediaType
* @return the MediaType of content.
*/
public MediaType getMediaType(ContentPacketExtension content)
{
String contentName = content.getName();
if (contentName == null)
return null;
MediaType mediaType = JingleUtils.getMediaType(content);
if (mediaType == null)
{
CallPeerMediaHandlerJabberImpl mediaHandler = getMediaHandler();
for (MediaType m : MediaType.values())
{
ContentPacketExtension sessionContent
= mediaHandler.getRemoteContent(m.toString());
if (sessionContent == null)
sessionContent = mediaHandler.getLocalContent(m.toString());
if (sessionContent != null
&& contentName.equals(sessionContent.getName()))
{
mediaType = m;
break;
}
}
}
return mediaType;
}
}