/* * 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.beans.*; import java.util.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.service.protocol.event.*; import net.java.sip.communicator.util.*; import org.jitsi.service.neomedia.*; import org.jitsi.util.xml.*; import org.w3c.dom.*; /** * Represents a default implementation of * OperationSetTelephonyConferencing in order to make it easier for * implementers to provide complete solutions while focusing on * implementation-specific details. * * @param * @param * @param * @param * @param * * @author Lyubomir Marinov * @author Boris Grozev */ public abstract class AbstractOperationSetTelephonyConferencing< ProtocolProviderServiceT extends ProtocolProviderService, OperationSetBasicTelephonyT extends OperationSetBasicTelephony, MediaAwareCallT extends MediaAwareCall< MediaAwareCallPeerT, OperationSetBasicTelephonyT, ProtocolProviderServiceT>, MediaAwareCallPeerT extends MediaAwareCallPeer< MediaAwareCallT, ?, ProtocolProviderServiceT>, CalleeAddressT extends Object> implements OperationSetTelephonyConferencing, RegistrationStateChangeListener, PropertyChangeListener, CallListener, CallChangeListener { /** * The Logger used by the * AbstractOperationSetTelephonyConferencing class and its * instances. */ private static final Logger logger = Logger.getLogger(AbstractOperationSetTelephonyConferencing.class); /** * The name of the conference-info XML element display-text. */ protected static final String ELEMENT_DISPLAY_TEXT = "display-text"; /** * The name of the conference-info XML element endpoint. */ protected static final String ELEMENT_ENDPOINT = "endpoint"; /** * The name of the conference-info XML element media. */ protected static final String ELEMENT_MEDIA = "media"; /** * The name of the conference-info XML element src-id. */ protected static final String ELEMENT_SRC_ID = "src-id"; /** * The name of the conference-info XML element status. */ protected static final String ELEMENT_STATUS = "status"; /** * The name of the conference-info XML element type. */ protected static final String ELEMENT_TYPE = "type"; /** * The name of the conference-info XML element user. */ protected static final String ELEMENT_USER = "user"; /** * The name of the conference-info XML element users. */ protected static final String ELEMENT_USERS = "users"; /** * The name of the account property which specifies whether we should * generate and send RFC4575 partial notifications (as opposed to always * sending 'full' documents) */ private static final String PARTIAL_NOTIFICATIONS_PROP_NAME = "RFC4575_PARTIAL_NOTIFICATIONS_ENABLED"; /** * The OperationSetBasicTelephony implementation which this * instance uses to carry out tasks such as establishing Calls. */ private OperationSetBasicTelephonyT basicTelephony; /** * The CallPeerListener which listens to modifications in the * properties/state of CallPeer so that NOTIFY requests can be sent * from a conference focus to its conference members to update them with * the latest information about the CallPeer. */ private final CallPeerListener callPeerListener = new CallPeerAdapter() { /** * Indicates that a change has occurred in the status of the source * CallPeer. * * @param evt the CallPeerChangeEvent instance containing the * source event as well as its previous and its new status */ @Override public void peerStateChanged(CallPeerChangeEvent evt) { CallPeer peer = evt.getSourceCallPeer(); if (peer != null) { Call call = peer.getCall(); if (call != null) { CallPeerState state = peer.getState(); if ((state != null) && !state.equals(CallPeerState.DISCONNECTED) && !state.equals(CallPeerState.FAILED)) { AbstractOperationSetTelephonyConferencing.this .notifyAll(call); } } } } }; /** * The ProtocolProviderService implementation which created this * instance and for which telephony conferencing services are being provided * by this instance. */ protected final ProtocolProviderServiceT parentProvider; /** * Initializes a new AbstractOperationSetTelephonyConferencing * instance which is to provide telephony conferencing services for the * specified ProtocolProviderService implementation. * * @param parentProvider the ProtocolProviderService implementation * which has requested the creation of the new instance and for which the * new instance is to provide telephony conferencing services */ protected AbstractOperationSetTelephonyConferencing( ProtocolProviderServiceT parentProvider) { this.parentProvider = parentProvider; this.parentProvider.addRegistrationStateChangeListener(this); } /** * Notifies this OperationSetTelephonyConferencing that its * basicTelephony property has changed its value from a specific * oldValue to a specific newValue * * @param oldValue the old value of the basicTelephony property * @param newValue the new value of the basicTelephony property */ protected void basicTelephonyChanged( OperationSetBasicTelephonyT oldValue, OperationSetBasicTelephonyT newValue) { if (oldValue != null) oldValue.removeCallListener(this); if (newValue != null) newValue.addCallListener(this); } /** * Notifies this CallListener that a specific Call has * been established. * * @param event a CallEvent which specified the newly-established * Call */ protected void callBegun(CallEvent event) { Call call = event.getSourceCall(); call.addCallChangeListener(this); /* * If there were any CallPeers in the Call prior to our realization that * it has begun, pretend that they are added afterwards. */ Iterator callPeerIter = call.getCallPeers(); while (callPeerIter.hasNext()) { callPeerAdded( new CallPeerEvent( callPeerIter.next(), call, CallPeerEvent.CALL_PEER_ADDED)); } } /** * Notifies this CallListener that a specific Call has * ended. * * @param event a CallEvent which specified the Call which * has just ended */ public void callEnded(CallEvent event) { Call call = event.getSourceCall(); /* * If there are still CallPeers after our realization that it has ended, * pretend that they are removed before that. */ Iterator callPeerIter = call.getCallPeers(); while (callPeerIter.hasNext()) { callPeerRemoved( new CallPeerEvent( callPeerIter.next(), call, CallPeerEvent.CALL_PEER_REMOVED)); } call.removeCallChangeListener(this); } /** * Notifies this CallChangeListener that a specific * CallPeer has been added to a specific Call. * * @param event a CallPeerEvent which specifies the * CallPeer which has been added to a Call */ public void callPeerAdded(CallPeerEvent event) { MediaAwareCallPeer callPeer = (MediaAwareCallPeer)event.getSourceCallPeer(); callPeer.addCallPeerListener(callPeerListener); callPeer.getMediaHandler().addPropertyChangeListener(this); callPeersChanged(event); } /** * Notifies this CallChangeListener that a specific * CallPeer has been remove from a specific Call. * * @param event a CallPeerEvent which specifies the * CallPeer which has been removed from a Call */ public void callPeerRemoved(CallPeerEvent event) { @SuppressWarnings("unchecked") MediaAwareCallPeerT callPeer = (MediaAwareCallPeerT) event.getSourceCallPeer(); callPeer.removeCallPeerListener(callPeerListener); callPeer.getMediaHandler().removePropertyChangeListener(this); callPeersChanged(event); } /** * Notifies this CallChangeListener that the CallPeer list * of a specific Call has been modified by adding or removing a * specific CallPeer. * * @param event a CallPeerEvent which specifies the * CallPeer which has been added to or removed from a Call */ private void callPeersChanged(CallPeerEvent event) { notifyAll(event.getSourceCall()); } /** * Notifies this CallChangeListener that a specific Call * has changed its state. Does nothing. * * @param event a CallChangeEvent which specifies the Call * which has changed its state, the very state which has been changed and * the values of the state before and after the change */ public void callStateChanged(CallChangeEvent event) { if (CallChangeEvent.CALL_PARTICIPANTS_CHANGE .equals(event.getPropertyName())) { notifyAll(event.getSourceCall()); } } /** * Creates a conference call with the specified callees as call peers. * * @param callees the list of addresses that we should call * @return the newly created conference call containing all CallPeers * @throws OperationFailedException if establishing the conference call * fails * @see OperationSetTelephonyConferencing#createConfCall(String[]) */ public Call createConfCall(String[] callees) throws OperationFailedException { return createConfCall(callees, null); } /** * Creates a conference Call with the specified callees as * CallPeers. * * @param callees the list of addresses that we should call * @param conference the CallConference which represents the state * of the telephony conference into which the specified callees are to be * invited * @return the newly-created conference call containing all * CallPeers * @throws OperationFailedException if establishing the conference * Call fails */ public Call createConfCall(String[] callees, CallConference conference) throws OperationFailedException { List calleeAddresses = new ArrayList(callees.length); for (String callee : callees) calleeAddresses.add(parseAddressString(callee)); MediaAwareCallT call = createOutgoingCall(); if (conference == null) conference = call.getConference(); else call.setConference(conference); conference.setConferenceFocus(true); for (CalleeAddressT calleeAddress : calleeAddresses) doInviteCalleeToCall(calleeAddress, call); return call; } /** * Creates a new outgoing Call into which conference callees are to * be invited by this OperationSetTelephonyConferencing. * * @return a new outgoing Call into which conference callees are to * be invited by this OperationSetTelephonyConferencing * @throws OperationFailedException if anything goes wrong */ protected abstract MediaAwareCallT createOutgoingCall() throws OperationFailedException; /** * Invites a callee with a specific address to join a specific Call * for the purposes of telephony conferencing. * * @param calleeAddress the address of the callee to be invited to the * specified existing Call * @param call the existing Call to invite the callee with the * specified address to * @return a new CallPeer instance which describes the signaling * and the media streaming of the newly-invited callee within the specified * Call * @throws OperationFailedException if inviting the specified callee to the * specified Call fails */ protected abstract CallPeer doInviteCalleeToCall( CalleeAddressT calleeAddress, MediaAwareCallT call) throws OperationFailedException; /** * Gets the OperationSetBasicTelephony implementation which this * instance uses to carry out tasks such as establishing Calls. * * @return the OperationSetBasicTelephony implementation which this * instance uses to carry out tasks such as establishing Calls */ public OperationSetBasicTelephonyT getBasicTelephony() { return basicTelephony; } private void getEndpointMediaProperties( Node endpoint, Map properties) { NodeList endpointChildList = endpoint.getChildNodes(); int endpoingChildCount = endpointChildList.getLength(); for (int endpointChildIndex = 0; endpointChildIndex < endpoingChildCount; endpointChildIndex++) { Node endpointChild = endpointChildList.item(endpointChildIndex); if (ELEMENT_MEDIA.equals(endpointChild.getNodeName())) { NodeList mediaChildList = endpointChild.getChildNodes(); int mediaChildCount = mediaChildList.getLength(); String srcId = null; String status = null; String type = null; for (int mediaChildIndex = 0; mediaChildIndex < mediaChildCount; mediaChildIndex++) { Node mediaChild = mediaChildList.item(mediaChildIndex); String mediaChildName = mediaChild.getNodeName(); if (ELEMENT_SRC_ID.equals(mediaChildName)) srcId = mediaChild.getTextContent(); else if (ELEMENT_STATUS.equals(mediaChildName)) status = mediaChild.getTextContent(); else if (ELEMENT_TYPE.equals(mediaChildName)) type = mediaChild.getTextContent(); } if (MediaType.AUDIO.toString().equalsIgnoreCase(type)) { properties.put( ConferenceMember.AUDIO_SSRC_PROPERTY_NAME, srcId); properties.put( ConferenceMember.AUDIO_STATUS_PROPERTY_NAME, status); } else if (MediaType.VIDEO.toString().equalsIgnoreCase(type)) { properties.put( ConferenceMember.VIDEO_SSRC_PROPERTY_NAME, srcId); properties.put( ConferenceMember.VIDEO_STATUS_PROPERTY_NAME, status); } } } } /** * Reads the text content of the status XML element of a specific * endpoint XML element. * * @param endpoint an XML Node which represents the * endpoint XML element from which to get the text content of its * status XML element * @return the text content of the status XML element of the * specified endpoint XML element if any; otherwise, null */ private String getEndpointStatus(Node endpoint) { NodeList childNodes = endpoint.getChildNodes(); int childCount = childNodes.getLength(); for (int i = 0; i < childCount; i++) { Node child = childNodes.item(i); if (ELEMENT_STATUS.equals(child.getNodeName())) return child.getTextContent(); } return null; } /** * Gets the remote SSRC to be reported in the conference-info XML for a * specific CallPeer's media of a specific MediaType. * * @param callPeer the CallPeer whose remote SSRC for the media of * the specified mediaType is to be returned * @param mediaType the MediaType of the specified * callPeer's media whose remote SSRC is to be returned * @return the remote SSRC to be reported in the conference-info XML for the * specified callPeer's media of the specified mediaType */ protected long getRemoteSourceID( MediaAwareCallPeer callPeer, MediaType mediaType) { long remoteSourceID = callPeer.getMediaHandler().getRemoteSSRC(mediaType); if (remoteSourceID != -1) { /* * TODO Technically, we are detecting conflicts within a Call * while we should be detecting them within the whole * CallConference. */ MediaAwareCall call = callPeer.getCall(); if (call != null) { for (MediaAwareCallPeer aCallPeer : call.getCallPeerList()) { if (aCallPeer != callPeer) { long aRemoteSourceID = aCallPeer.getMediaHandler().getRemoteSSRC( mediaType); if (aRemoteSourceID == remoteSourceID) { remoteSourceID = -1; break; } } } } } return remoteSourceID; } /** * Notifies this CallListener that a specific incoming * Call has been received. * * @param event a CallEvent which specifies the newly-received * incoming Call */ public void incomingCallReceived(CallEvent event) { callBegun(event); } /** * Invites the callee represented by the specified uri to an already * existing call. The difference between this method and createConfCall is * that inviteCalleeToCall allows a user to transform an existing 1 to 1 * call into a conference call, or add new peers to an already established * conference. * * @param uri the callee to invite to an existing conf call. * @param call the call that we should invite the callee to. * @return the CallPeer object corresponding to the callee represented by * the specified uri. * @throws OperationFailedException if inviting the specified callee to the * specified call fails */ public CallPeer inviteCalleeToCall(String uri, Call call) throws OperationFailedException { CalleeAddressT calleeAddress = parseAddressString(uri); @SuppressWarnings("unchecked") MediaAwareCallT mediaAwareCallT = (MediaAwareCallT) call; mediaAwareCallT.getConference().setConferenceFocus(true); return doInviteCalleeToCall(calleeAddress, mediaAwareCallT); } /** * Notifies all CallPeers associated with the telephony conference * in which a specific Call is participating about changes in the * telephony conference-related information. * * @param call the Call which specifies the telephony conference * the associated CallPeers of which are to be notified about * changes in the telephony conference-related information */ @SuppressWarnings("rawtypes") protected void notifyAll(Call call) { CallConference conference = call.getConference(); if (conference == null) notifyCallPeers(call); else { /* * Make each Call notify its CallPeers through its * OperationSetTelephonyConferencing (i.e. its protocol). */ for (Call conferenceCall : conference.getCalls()) { OperationSetTelephonyConferencing opSet = conferenceCall.getProtocolProvider().getOperationSet( OperationSetTelephonyConferencing.class); if (opSet instanceof AbstractOperationSetTelephonyConferencing) { ((AbstractOperationSetTelephonyConferencing) opSet) .notifyCallPeers(conferenceCall); } } } } /** * Notifies all CallPeers associated with a specific Call * about changes in the telephony conference-related information. In * contrast, {@link #notifyAll()} notifies all CallPeers associated * with the telephony conference in which a specific Call is * participating. * * @param call the Call whose CallPeers are to be notified * about changes in the telephony conference-related information */ protected abstract void notifyCallPeers(Call call); /** * Notifies this CallListener that a specific outgoing * Call has been created. * * @param event a CallEvent which specifies the newly-created * outgoing Call */ public void outgoingCallCreated(CallEvent event) { callBegun(event); } /** * Parses a String value which represents a callee address * specified by the user into an object which is to actually represent the * callee during the invitation to a conference Call. * * @param calleeAddressString a String value which represents a * callee address to be parsed into an object which is to actually represent * the callee during the invitation to a conference Call * @return an object which is to actually represent the specified * calleeAddressString during the invitation to a conference * Call * @throws OperationFailedException if parsing the specified * calleeAddressString fails */ protected abstract CalleeAddressT parseAddressString( String calleeAddressString) throws OperationFailedException; /** * Notifies this PropertyChangeListener that the value of a * specific property of the notifier it is registered with has changed. * * @param ev a PropertyChangeEvent which describes the source of * the event, the name of the property which has changed its value and the * old and new values of the property * @see PropertyChangeListener#propertyChange(PropertyChangeEvent) */ public void propertyChange(PropertyChangeEvent ev) { String propertyName = ev.getPropertyName(); if (CallPeerMediaHandler.AUDIO_LOCAL_SSRC.equals(propertyName) || CallPeerMediaHandler.AUDIO_REMOTE_SSRC.equals(propertyName) || CallPeerMediaHandler.VIDEO_LOCAL_SSRC.equals(propertyName) || CallPeerMediaHandler.VIDEO_REMOTE_SSRC.equals(propertyName)) { @SuppressWarnings("unchecked") CallPeerMediaHandler mediaHandler = (CallPeerMediaHandler) ev.getSource(); Call call = mediaHandler.getPeer().getCall(); if (call != null) notifyAll(call); } } /** * Notifies this RegistrationStateChangeListener that the * ProtocolProviderSerivce it is registered with has changed its * registration state. * * @param event a RegistrationStateChangeEvent which specifies the * old and the new value of the registration state of the * ProtocolProviderService this * RegistrationStateChangeListener listens to */ public void registrationStateChanged(RegistrationStateChangeEvent event) { RegistrationState newState = event.getNewState(); if (RegistrationState.REGISTERED.equals(newState)) { @SuppressWarnings("unchecked") OperationSetBasicTelephonyT basicTelephony = (OperationSetBasicTelephonyT) parentProvider.getOperationSet( OperationSetBasicTelephony.class); if (this.basicTelephony != basicTelephony) { OperationSetBasicTelephonyT oldValue = this.basicTelephony; this.basicTelephony = basicTelephony; basicTelephonyChanged(oldValue, this.basicTelephony); } } else if (RegistrationState.UNREGISTERED.equals(newState)) { if (basicTelephony != null) { OperationSetBasicTelephonyT oldValue = basicTelephony; basicTelephony = null; basicTelephonyChanged(oldValue, null); } } } /** * Updates the conference-related properties of a specific CallPeer * such as conferenceFocus and conferenceMembers with * the information described in confInfo. * confInfo must be a document with "full" state. * * @param callPeer the CallPeer which is a conference focus and has * sent the specified conference-info XML document * @param confInfo the conference-info XML document to use to update * the conference-related information of the local peer represented * by the associated Call. It must have a "full" state. */ private int setConferenceInfoDocument( MediaAwareCallPeerT callPeer, ConferenceInfoDocument confInfo) { NodeList usersList = confInfo.getDocument().getElementsByTagName(ELEMENT_USERS); ConferenceMember[] toRemove = callPeer.getConferenceMembers().toArray( AbstractCallPeer.NO_CONFERENCE_MEMBERS); int toRemoveCount = toRemove.length; boolean changed = false; if (usersList.getLength() > 0) { NodeList userList = usersList.item(0).getChildNodes(); int userCount = userList.getLength(); Map conferenceMemberProperties = new HashMap(); for (int userIndex = 0; userIndex < userCount; userIndex++) { Node user = userList.item(userIndex); if (!ELEMENT_USER.equals(user.getNodeName())) continue; String address = stripParametersFromAddress( ((Element) user).getAttribute("entity")); if ((address == null) || (address.length() < 1)) continue; /* * Determine the ConferenceMembers who are no longer in the list * i.e. are to be removed. */ AbstractConferenceMember conferenceMember = null; for (int i = 0; i < toRemoveCount; i++) { ConferenceMember aConferenceMember = toRemove[i]; if ((aConferenceMember != null) && address.equalsIgnoreCase( aConferenceMember.getAddress())) { toRemove[i] = null; conferenceMember = (AbstractConferenceMember) aConferenceMember; break; } } // Create the new ones. boolean addConferenceMember; if (conferenceMember == null) { conferenceMember = new AbstractConferenceMember(callPeer, address); addConferenceMember = true; } else addConferenceMember = false; // Update the existing ones. if (conferenceMember != null) { NodeList userChildList = user.getChildNodes(); int userChildCount = userChildList.getLength(); String displayName = null; String endpointStatus = null; conferenceMemberProperties.put( ConferenceMember.AUDIO_SSRC_PROPERTY_NAME, null); conferenceMemberProperties.put( ConferenceMember.AUDIO_STATUS_PROPERTY_NAME, null); conferenceMemberProperties.put( ConferenceMember.VIDEO_SSRC_PROPERTY_NAME, null); conferenceMemberProperties.put( ConferenceMember.VIDEO_STATUS_PROPERTY_NAME, null); for (int userChildIndex = 0; userChildIndex < userChildCount; userChildIndex++) { Node userChild = userChildList.item(userChildIndex); String userChildName = userChild.getNodeName(); if (ELEMENT_DISPLAY_TEXT.equals(userChildName)) displayName = userChild.getTextContent(); else if (ELEMENT_ENDPOINT.equals(userChildName)) { endpointStatus = getEndpointStatus(userChild); getEndpointMediaProperties( userChild, conferenceMemberProperties); } } conferenceMember.setDisplayName(displayName); conferenceMember.setEndpointStatus(endpointStatus); changed = conferenceMember.setProperties( conferenceMemberProperties); if (addConferenceMember) callPeer.addConferenceMember(conferenceMember); } } } /* * Remove the ConferenceMember instances which are no longer present in * the conference-info XML document. */ for (int i = 0; i < toRemoveCount; i++) { ConferenceMember conferenceMemberToRemove = toRemove[i]; if (conferenceMemberToRemove != null) callPeer.removeConferenceMember(conferenceMemberToRemove); } if (changed) notifyAll(callPeer.getCall()); callPeer.setLastConferenceInfoReceived(confInfo); return confInfo.getVersion(); } /** * Updates the conference-related properties of a specific CallPeer * such as conferenceFocus and conferenceMembers with * information received from it as a conference focus in the form of a * conference-info XML document. * * @param callPeer the CallPeer which is a conference focus and has * sent the specified conference-info XML document * @param conferenceInfoXML the conference-info XML document sent by * callPeer in order to update the conference-related information * of the local peer represented by the associated Call * @return the value of the version attribute of the * conference-info XML element of the specified * conferenceInfoXML if it was successfully parsed and represented * in the specified callPeer * * @throws XMLException If conferenceInfoXML couldn't be parsed as * a ConferenceInfoDocument */ protected int setConferenceInfoXML( MediaAwareCallPeerT callPeer, String conferenceInfoXML) throws XMLException { ConferenceInfoDocument confInfo = new ConferenceInfoDocument(conferenceInfoXML); /* * The CallPeer sent conference-info XML so we're sure it's a * conference focus. */ callPeer.setConferenceFocus(true); /* * The following implements the procedure outlined in section 4.6 of * RFC4575 - Constructing Coherent State */ int documentVersion = confInfo.getVersion(); int ourVersion = callPeer.getLastConferenceInfoReceivedVersion(); ConferenceInfoDocument.State documentState = confInfo.getState(); if (ourVersion == -1) { if (documentState == ConferenceInfoDocument.State.FULL) { return setConferenceInfoDocument(callPeer, confInfo); } else { logger.warn("Received a conference-info document with state '" + documentState + "'. Cannot apply it, because we haven't " + "initialized a local document yet. Sending peer: " + callPeer); return -1; } } else if (documentVersion <= ourVersion) { if (logger.isInfoEnabled()) { logger.info("Received a stale conference-info document. Local " + "version " + ourVersion + ", document version " + documentVersion + ". Sending peer: " + callPeer); } return -1; } else //ourVersion != -1 && ourVersion < documentVersion { if (documentState == ConferenceInfoDocument.State.FULL) return setConferenceInfoDocument(callPeer, confInfo); else if (documentState == ConferenceInfoDocument.State.DELETED) { logger.warn("Received a conference-info document with state" + "'deleted', can't handle. Sending peer: " + callPeer); return -1; } else if (documentState == ConferenceInfoDocument.State.PARTIAL) { if (documentVersion == ourVersion+1) return updateConferenceInfoDocument(callPeer, confInfo); else { /* * According to RFC4575 we "MUST generate a subscription * refresh request to trigger a full state notification". */ logger.warn("Received a Conference Information document " + "with state '" + documentState + "' and version " + documentVersion + ". Cannon apply it, because local " + "version is " + ourVersion + ". Sending peer: " + callPeer); return -1; } } else return -1; //unreachable } } /** * Removes the parameters (specified after a semicolon) from a specific * address String if any are present in it. * * @param address the String value representing an address from * which any parameters are to be removed * @return a String representing the specified address * without any parameters */ public static String stripParametersFromAddress(String address) { if (address != null) { int parametersBeginIndex = address.indexOf(';'); if (parametersBeginIndex > -1) address = address.substring(0, parametersBeginIndex); } return address; } /** * Creates a ConferenceInfoDocument which describes the current * state of the conference in which callPeer participates. The * created document contains a "full" description (as opposed to a partial * description, see RFC4575). * * @return a ConferenceInfoDocument which describes the current * state of the conference in which this CallPeer participates. */ protected ConferenceInfoDocument getCurrentConferenceInfo( MediaAwareCallPeer callPeer) { ConferenceInfoDocument confInfo; try { confInfo = new ConferenceInfoDocument(); } catch (XMLException e) { return null; } confInfo.setState(ConferenceInfoDocument.State.FULL); confInfo.setEntity(getLocalEntity(callPeer)); Call call = callPeer.getCall(); if (call == null) return null; List conferenceCallPeers = CallConference.getCallPeers(call); confInfo.setUserCount( 1 /* the local peer/user */ + conferenceCallPeers.size()); /* The local user */ addPeerToConferenceInfo(confInfo, callPeer, false); /* Remote users */ for (CallPeer conferenceCallPeer : conferenceCallPeers) { if (conferenceCallPeer instanceof MediaAwareCallPeer) { addPeerToConferenceInfo( confInfo, (MediaAwareCallPeer)conferenceCallPeer, true); } } return confInfo; } /** * Adds a user element to confInfo which describes * callPeer, or the local peer if remote is false. * * @param confInfo the ConferenceInformationDocument to which to * add a user element * @param callPeer the CallPeer which should be described * @param remote true to describe callPeer, or * false to describe the local peer. */ private void addPeerToConferenceInfo( ConferenceInfoDocument confInfo, MediaAwareCallPeer callPeer, boolean remote) { String entity = remote ? callPeer.getEntity() : getLocalEntity(callPeer); ConferenceInfoDocument.User user = confInfo.addNewUser(entity); String displayName = remote ? callPeer.getDisplayName() : getLocalDisplayName(); user.setDisplayText(displayName); ConferenceInfoDocument.Endpoint endpoint = user.addNewEndpoint(entity); endpoint.setStatus( remote ? getEndpointStatus(callPeer) : ConferenceInfoDocument.EndpointStatusType.connected); CallPeerMediaHandler mediaHandler = callPeer.getMediaHandler(); for (MediaType mediaType : MediaType.values()) { MediaStream stream = mediaHandler.getStream(mediaType); if (stream != null || !remote) { long srcId = -1; if (remote) { srcId = getRemoteSourceID(callPeer, mediaType); } else if (stream != null) { srcId = stream.getLocalSourceID(); } else // stream == null && !remote { /* * If we are describing the local peer, but we don't have * media streams with callPeer (which is the case when we * send conference-info while the other side is still * ringing), we can try to obtain our local SSRC from the * streams we have already set up with the other * participants in the conference. */ for (MediaAwareCallPeer otherCallPeer : callPeer.getCall().getCallPeerList()) { MediaStream otherStream = otherCallPeer.getMediaHandler().getStream( mediaType); if (otherStream != null) { srcId = otherStream.getLocalSourceID(); break; } } } MediaDirection direction = MediaDirection.INACTIVE; if (remote) { direction = callPeer.getDirection(mediaType).getReverseDirection(); } else { if (mediaType == MediaType.AUDIO && callPeer.getMediaHandler() .isLocalAudioTransmissionEnabled()) direction = direction.or(MediaDirection.SENDONLY); else if (mediaType == MediaType.VIDEO && callPeer.isLocalVideoStreaming()) direction = direction.or(MediaDirection.SENDONLY); if (callPeer.getDirection(mediaType).allowsReceiving()) direction = direction.or(MediaDirection.RECVONLY); } if ((srcId != -1) || (direction != MediaDirection.INACTIVE)) { ConferenceInfoDocument.Media media = endpoint.addNewMedia(mediaType.toString()); media.setType(mediaType.toString()); if (srcId != -1) media.setSrcId(Long.toString(srcId)); media.setStatus(direction.toString()); } } } } /** * Returns a string to be used for the entity attribute of the * user element for the local peer, in a Conference Information * document to be sent to callPeer * * @param callPeer The CallPeer for which we are creating a * Conference Information document. * @return a string to be used for the entity attribute of the * user element for the local peer, in a Conference Information * document to be sent to callPeer */ protected abstract String getLocalEntity(CallPeer callPeer); /** * Returns the display name for the local peer, which is to be used when * we send Conference Information. * @return the display name for the local peer, which is to be used when * we send Conference Information. */ protected abstract String getLocalDisplayName(); /** * Gets the EndpointStatusType to use when describing * callPeer in a Conference Information document. * * @param callPeer the CallPeer which is to get its state described * in a status XML element of an endpoint XML element * @return the EndpointStatusType to use when describing * callPeer in a Conference Information document. */ private ConferenceInfoDocument.EndpointStatusType getEndpointStatus( CallPeer callPeer) { CallPeerState callPeerState = callPeer.getState(); if (CallPeerState.ALERTING_REMOTE_SIDE.equals(callPeerState)) return ConferenceInfoDocument.EndpointStatusType.alerting; if (CallPeerState.CONNECTING.equals(callPeerState) || CallPeerState .CONNECTING_WITH_EARLY_MEDIA.equals(callPeerState)) return ConferenceInfoDocument.EndpointStatusType.pending; if (CallPeerState.DISCONNECTED.equals(callPeerState)) return ConferenceInfoDocument.EndpointStatusType.disconnected; if (CallPeerState.INCOMING_CALL.equals(callPeerState)) return ConferenceInfoDocument.EndpointStatusType.dialing_in; if (CallPeerState.INITIATING_CALL.equals(callPeerState)) return ConferenceInfoDocument.EndpointStatusType.dialing_out; /* * RFC4575 does not list an appropriate endpoint status for * "remotely on hold", e.g. the endpoint is not "hearing" the conference * mix, but it's media stream *is* being mixed into the conference. * * We use the on-hold status anyway, because it's the one that makes * the most sense. */ if (CallPeerState.ON_HOLD_REMOTELY.equals(callPeerState)) return ConferenceInfoDocument.EndpointStatusType.on_hold; /* * he/she is neither "hearing" the conference mix nor is his/her * media being mixed in the conference */ if (CallPeerState.ON_HOLD_LOCALLY.equals(callPeerState) || CallPeerState.ON_HOLD_MUTUALLY.equals(callPeerState)) return ConferenceInfoDocument.EndpointStatusType.on_hold; if (CallPeerState.CONNECTED.equals(callPeerState)) return ConferenceInfoDocument.EndpointStatusType.connected; return null; } /** * @param from A document with state full from which to generate a * "diff". * @param to A document with state full to which to generate a * "diff" * @return a ConferenceInfoDocument, such that when it is applied * to from using the procedure defined in section 4.6 of RFC4575, * the result is to. May return null if from and * to are not found to be different (that is, in case no document * needs to be sent) */ protected ConferenceInfoDocument getConferenceInfoDiff( ConferenceInfoDocument from, ConferenceInfoDocument to) throws IllegalArgumentException { if (from.getState() != ConferenceInfoDocument.State.FULL) throw new IllegalArgumentException("The 'from' document needs to " + "have state=full"); if (to.getState() != ConferenceInfoDocument.State.FULL) throw new IllegalArgumentException("The 'to' document needs to " + "have state=full"); if (!isPartialNotificationEnabled()) { return conferenceInfoDocumentsMatch(from, to) ? null : to; } ConferenceInfoDocument diff; try { diff = new ConferenceInfoDocument(); } catch (XMLException e) { return conferenceInfoDocumentsMatch(from, to) ? null : to; } diff.setState(ConferenceInfoDocument.State.PARTIAL); diff.setUsersState(ConferenceInfoDocument.State.PARTIAL); //temporary, used for xmpp only String sid = to.getSid(); if (sid != null && !sid.equals("")) diff.setSid(to.getSid()); diff.setUserCount(to.getUserCount()); diff.setEntity(to.getEntity()); boolean needsPartial = false; boolean hasDifference = false; if (!from.getEntity().equals(to.getEntity()) || from.getUserCount() != to.getUserCount()) { hasDifference = true; } // find users which have been removed in 'to' for (ConferenceInfoDocument.User user : from.getUsers()) { if(to.getUser(user.getEntity()) == null) { ConferenceInfoDocument.User deletedUser = diff.addNewUser(user.getEntity()); deletedUser.setState(ConferenceInfoDocument.State.DELETED); hasDifference = true; needsPartial = true; } } for (ConferenceInfoDocument.User toUser : to.getUsers()) { ConferenceInfoDocument.User fromUser = from.getUser(toUser.getEntity()); if (!usersMatch(toUser, fromUser)) { hasDifference = true; diff.addUser(toUser); } else { //if there is a "user" element which didn't change, we skip it //and we need to send state=partial, because otherwise it will //be removed by the recipient needsPartial = true; } } if (logger.isDebugEnabled()) { logger.debug("Generated partial notification. From: " + from + "\nTo: " + to + "\nDiff: " + diff + "(hasDifference: " + hasDifference + ")"); } if (!hasDifference) return null; /* * In some cases (when all the user elements have changed, and none have * been removed) we are essentially generating a full document, but * marking it 'partial'. In this case it is better to send the full * document, just in case the receiver lost the previous document * somehow. */ if (!needsPartial) { diff.setState(ConferenceInfoDocument.State.FULL); diff.setUsersState(ConferenceInfoDocument.State.FULL); } return diff; } /** * Updates the conference-related properties of a specific CallPeer * such as conferenceFocus and conferenceMembers with * information received from it as a conference focus in the form of a * partial conference-info XML document. * * @param callPeer the CallPeer which is a conference focus and has * sent the specified partial conference-info XML document * @param diff the partial conference-info XML document sent by * callPeer in order to update the conference-related information * of the local peer represented by the associated Call * @return the value of the version attribute of the * conference-info XML element of the specified * conferenceInfoXML if it was successfully parsed and represented * in the specified callPeer */ private int updateConferenceInfoDocument( MediaAwareCallPeerT callPeer, ConferenceInfoDocument diff) { // "apply" diff to ourDocument, result is in newDocument ConferenceInfoDocument ourDocument = callPeer.getLastConferenceInfoReceived(); ConferenceInfoDocument newDocument; ConferenceInfoDocument.State usersState = diff.getUsersState(); if (usersState == ConferenceInfoDocument.State.FULL) { //if users is 'full', all its children must be full try { newDocument = new ConferenceInfoDocument(diff); } catch (XMLException e) { logger.error("Could not create a new ConferenceInfoDocument"); return -1; } newDocument.setState(ConferenceInfoDocument.State.FULL); } else if (usersState == ConferenceInfoDocument.State.DELETED) { try { newDocument = new ConferenceInfoDocument(); } catch (XMLException e) { logger.error("Could not create a new ConferenceInfoDocument", e); return -1; } newDocument.setVersion(diff.getVersion()); newDocument.setEntity(diff.getEntity()); newDocument.setUserCount(diff.getUserCount()); } else //'partial' { try { newDocument = new ConferenceInfoDocument(ourDocument); } catch (XMLException e) { logger.error("Could not create a new ConferenceInfoDocument", e); return -1; } newDocument.setVersion(diff.getVersion()); newDocument.setEntity(diff.getEntity()); newDocument.setUserCount(diff.getUserCount()); for (ConferenceInfoDocument.User user : diff.getUsers()) { ConferenceInfoDocument.State userState = user.getState(); if (userState == ConferenceInfoDocument.State.FULL) { newDocument.removeUser(user.getEntity()); newDocument.addUser(user); } else if (userState == ConferenceInfoDocument.State.DELETED) { newDocument.removeUser(user.getEntity()); } else // partial { ConferenceInfoDocument.User ourUser = newDocument.getUser(user.getEntity()); ourUser.setDisplayText(user.getDisplayText()); for (ConferenceInfoDocument.Endpoint endpoint : user.getEndpoints()) { ConferenceInfoDocument.State endpointState = endpoint.getState(); if (endpointState == ConferenceInfoDocument.State.FULL) { ourUser.removeEndpoint(endpoint.getEntity()); ourUser.addEndpoint(endpoint); } else if (endpointState == ConferenceInfoDocument.State.DELETED) { ourUser.removeEndpoint(endpoint.getEntity()); } else // partial { ConferenceInfoDocument.Endpoint ourEndpoint = ourUser.getEndpoint(endpoint.getEntity()); for (ConferenceInfoDocument.Media media : endpoint.getMedias()) { ourEndpoint.removeMedia(media.getId()); ourEndpoint.addMedia(media); } } } } } } if (logger.isDebugEnabled()) { logger.debug("Applied a partial conference-info notification. " + " Base: " + ourDocument + "\nDiff: " + diff + "\nResult:" + newDocument); } return setConferenceInfoDocument(callPeer, newDocument); } /** * @param a A document with state full which to compare to * b * @param b A document with state full which to compare to * a * @return false if the two documents are found to be different, * true otherwise (that is, it can return true for non-identical * documents). */ private boolean conferenceInfoDocumentsMatch( ConferenceInfoDocument a, ConferenceInfoDocument b) { if (a.getState() != ConferenceInfoDocument.State.FULL) throw new IllegalArgumentException("The 'a' document needs to" + "have state=full"); if (b.getState() != ConferenceInfoDocument.State.FULL) throw new IllegalArgumentException("The 'b' document needs to" + "have state=full"); if (!stringsMatch(a.getEntity(), b.getEntity())) return false; else if (a.getUserCount() != b.getUserCount()) return false; else if (a.getUsers().size() != b.getUsers().size()) return false; for(ConferenceInfoDocument.User aUser : a.getUsers()) { if (!usersMatch(aUser, b.getUser(aUser.getEntity()))) return false; } return true; } /** * Checks whether two ConferenceInfoDocument.User instances * match according to the needs of our implementation. Can return * true for users which are not identical. * * @param a A ConferenceInfoDocument.User to compare * @param b A ConferenceInfoDocument.User to compare * @return false if a and b are found to be * different in a way that is significant for our needs, true * otherwise. */ private boolean usersMatch( ConferenceInfoDocument.User a, ConferenceInfoDocument.User b) { if (a == null && b == null) return true; else if (a == null || b == null) return false; else if (!stringsMatch(a.getEntity(), b.getEntity())) return false; else if (!stringsMatch(a.getDisplayText(), b.getDisplayText())) return false; else if (a.getEndpoints().size() != b.getEndpoints().size()) return false; for (ConferenceInfoDocument.Endpoint aEndpoint : a.getEndpoints()) { if (!endpointsMatch(aEndpoint, b.getEndpoint(aEndpoint.getEntity()))) return false; } return true; } /** * Checks whether two ConferenceInfoDocument.Endpoint instances * match according to the needs of our implementation. Can return * true for endpoints which are not identical. * * @param a A ConferenceInfoDocument.Endpoint to compare * @param b A ConferenceInfoDocument.Endpoint to compare * @return false if a and b are found to be * different in a way that is significant for our needs, true * otherwise. */ private boolean endpointsMatch( ConferenceInfoDocument.Endpoint a, ConferenceInfoDocument.Endpoint b) { if (a == null && b == null) return true; else if (a == null || b == null) return false; else if (!stringsMatch(a.getEntity(), b.getEntity())) return false; else if (a.getStatus() != b.getStatus()) return false; else if (a.getMedias().size() != b.getMedias().size()) return false; for (ConferenceInfoDocument.Media aMedia : a.getMedias()) { if (!mediasMatch(aMedia, b.getMedia(aMedia.getId()))) return false; } return true; } /** * Checks whether two ConferenceInfoDocument.Media instances * match according to the needs of our implementation. Can return * true for endpoints which are not identical. * * @param a A ConferenceInfoDocument.Media to compare * @param b A ConferenceInfoDocument.Media to compare * @return false if a and b are found to be * different in a way that is significant for our needs, true * otherwise. */ private boolean mediasMatch( ConferenceInfoDocument.Media a, ConferenceInfoDocument.Media b) { if (a == null && b == null) return true; else if (a == null || b == null) return false; else if (!stringsMatch(a.getId(), b.getId())) return false; else if (!stringsMatch(a.getSrcId(), b.getSrcId())) return false; else if (!stringsMatch(a.getType(), b.getType())) return false; else if (!stringsMatch(a.getStatus(), b.getStatus())) return false; return true; } /** * @param a A String to compare to b * @param b A String to compare to a * @return true if and only if a and b are both * null, or they are equal as Strings */ private boolean stringsMatch(String a, String b) { if (a == null && b == null) return true; else if (a == null || b == null) return false; return a.equals(b); } /** * Checks whether sending of RFC4575 partial notifications is enabled in * the configuration. If disabled, RFC4575 documents will always be sent * with state 'full'. * @return true if sending of RFC4575 partial notifications is * enabled in the configuration. */ private boolean isPartialNotificationEnabled() { String s = parentProvider.getAccountID() .getAccountProperties() .get(PARTIAL_NOTIFICATIONS_PROP_NAME); return (s == null || Boolean.parseBoolean(s)); } /** * {@inheritDoc} * * Unimplemented by default, returns null. */ @Override public ConferenceDescription setupConference(ChatRoom chatRoom) { return null; } }