diff --git a/lib/installer-exclude/libjitsi.jar b/lib/installer-exclude/libjitsi.jar index cee1f5f46..18f0e6c63 100644 Binary files a/lib/installer-exclude/libjitsi.jar and b/lib/installer-exclude/libjitsi.jar differ diff --git a/src/net/java/sip/communicator/impl/gui/main/call/CallManager.java b/src/net/java/sip/communicator/impl/gui/main/call/CallManager.java index a3ddc4ede..df5f7a1f5 100644 --- a/src/net/java/sip/communicator/impl/gui/main/call/CallManager.java +++ b/src/net/java/sip/communicator/impl/gui/main/call/CallManager.java @@ -1499,29 +1499,30 @@ public static boolean addressesAreEqual(String a, String b) return true; int aProtocolIndex = a.indexOf(':'); - if(aProtocolIndex > -1) + if(aProtocolIndex != -1) a = a.substring(aProtocolIndex + 1); int bProtocolIndex = b.indexOf(':'); - if(bProtocolIndex > -1) + if(bProtocolIndex != -1) b = b.substring(bProtocolIndex + 1); if (a.equals(b)) return true; - int aServiceBegin = a.indexOf('@'); + int aServiceBegin = a.indexOf('@', aProtocolIndex); String aUserID; String aService; - if (aServiceBegin > -1) + if (aServiceBegin != -1) { aUserID = a.substring(0, aServiceBegin); + ++aServiceBegin; - int slashIndex = a.indexOf("/"); - if (slashIndex > 0) - aService = a.substring(aServiceBegin + 1, slashIndex); + int aResourceBegin = a.indexOf('/', aServiceBegin); + if (aResourceBegin != -1) + aService = a.substring(aServiceBegin, aResourceBegin); else - aService = a.substring(aServiceBegin + 1); + aService = a.substring(aServiceBegin); } else { @@ -1529,19 +1530,20 @@ public static boolean addressesAreEqual(String a, String b) aService = null; } - int bServiceBegin = b.indexOf('@'); + int bServiceBegin = b.indexOf('@', bProtocolIndex); String bUserID; String bService; - if (bServiceBegin > -1) + if (bServiceBegin != -1) { bUserID = b.substring(0, bServiceBegin); - int slashIndex = b.indexOf("/"); + ++bServiceBegin; - if (slashIndex > 0) - bService = b.substring(bServiceBegin + 1, slashIndex); + int bResourceBegin = b.indexOf('/', bServiceBegin); + if (bResourceBegin != -1) + bService = b.substring(bServiceBegin, bResourceBegin); else - bService = b.substring(bServiceBegin + 1); + bService = b.substring(bServiceBegin); } else { diff --git a/src/net/java/sip/communicator/impl/gui/main/call/ZrtpSecurityPanel.java b/src/net/java/sip/communicator/impl/gui/main/call/ZrtpSecurityPanel.java index 02ac19f41..bacc2e3dd 100644 --- a/src/net/java/sip/communicator/impl/gui/main/call/ZrtpSecurityPanel.java +++ b/src/net/java/sip/communicator/impl/gui/main/call/ZrtpSecurityPanel.java @@ -9,6 +9,7 @@ import java.awt.*; import java.awt.event.*; import java.beans.*; +import java.util.List; import javax.swing.*; @@ -24,13 +25,14 @@ import org.jitsi.service.configuration.*; import org.jitsi.service.neomedia.*; import org.jitsi.service.protocol.event.*; +import org.jitsi.service.resources.*; import org.jitsi.util.event.*; /** * The panel containing details about ZRTP call security. * * @author Werner Dittman - * @author Lubomir Marinov + * @author Lyubomir Marinov * @author Yana Stamcheva */ public class ZrtpSecurityPanel @@ -273,30 +275,31 @@ private JPanel createSasPanel() { sasVerified = getSecurityControl().isSecurityVerified(); - TransparentPanel sasPanel = new TransparentPanel() - { - @Override - public void paintComponent(Graphics g) + TransparentPanel sasPanel + = new TransparentPanel() { - g = g.create(); - try - { - AntialiasingManager.activateAntialiasing(g); - g.setColor(new Color(1f, 1f, 1f, 0.1f)); - g.fillRoundRect( - 0, 0, this.getWidth(), this.getHeight(), 10, 10); - g.setColor(Color.WHITE); - g.drawRoundRect( - 0, 0, - this.getWidth() - 1, this.getHeight() - 1, - 10, 10); - } - finally + @Override + public void paintComponent(Graphics g) { - g.dispose(); + g = g.create(); + try + { + AntialiasingManager.activateAntialiasing(g); + g.setColor(new Color(1f, 1f, 1f, 0.1f)); + g.fillRoundRect( + 0, 0, this.getWidth(), this.getHeight(), 10, 10); + g.setColor(Color.WHITE); + g.drawRoundRect( + 0, 0, + this.getWidth() - 1, this.getHeight() - 1, + 10, 10); + } + finally + { + g.dispose(); + } } - } - }; + }; sasPanel.setLayout(new BoxLayout(sasPanel, BoxLayout.Y_AXIS)); @@ -592,57 +595,74 @@ private void setVideoSecurityOn(final boolean isVideoSecurityOn) { if (!SwingUtilities.isEventDispatchThread()) { - SwingUtilities.invokeLater(new Runnable() - { - public void run() - { - setVideoSecurityOn(isVideoSecurityOn); - } - }); + SwingUtilities.invokeLater( + new Runnable() + { + public void run() + { + setVideoSecurityOn(isVideoSecurityOn); + } + }); return; } this.isVideoSecurityOn = isVideoSecurityOn; - Icon statusIcon = null; - String statusText = null; + Icon icon = null; + String text = null; + boolean visible = false; - final OperationSetVideoTelephony telephony - = callPeer.getProtocolProvider() - .getOperationSet(OperationSetVideoTelephony.class); + OperationSetVideoTelephony videoTelephony + = callPeer.getProtocolProvider().getOperationSet( + OperationSetVideoTelephony.class); - if (telephony != null - && ((telephony.getVisualComponents(callPeer) != null - && telephony.getVisualComponents(callPeer).size() > 0) - || ((MediaAwareCallPeer) callPeer) - .isLocalVideoStreaming())) + if (videoTelephony != null) { - if (isVideoSecurityOn) + /* + * The invocation of MediaAwareCallPeer.isLocalVideoStreaming() is + * cheaper than the invocation of + * OperationSetVideoTelephony.getVisualComponents(CallPeer). + */ + visible + = ((MediaAwareCallPeer) callPeer) + .isLocalVideoStreaming(); + if (!visible) { - statusIcon = videoSecuredIcon; - statusText = GuiActivator.getResources() - .getI18NString("service.gui.security.SECURE_VIDEO"); + List videos + = videoTelephony.getVisualComponents(callPeer); + + visible = ((videos != null) && (videos.size() != 0)); } - else + + if (visible) { - statusIcon = videoNotSecuredIcon; - statusText = GuiActivator.getResources() - .getI18NString("service.gui.security.VIDEO_NOT_SECURED"); + ResourceManagementService r = GuiActivator.getResources(); + + if (isVideoSecurityOn) + { + icon = videoSecuredIcon; + text = r.getI18NString("service.gui.security.SECURE_VIDEO"); + } + else + { + icon = videoNotSecuredIcon; + text + = r.getI18NString( + "service.gui.security.VIDEO_NOT_SECURED"); + } } } - else - { - videoSecurityLabel.setVisible(false); - } - if (statusIcon != null && statusText != null) + if ((icon != null) && (text != null)) { - videoSecurityLabel.setIcon(statusIcon); - videoSecurityLabel.setText(statusText); + videoSecurityLabel.setIcon(icon); + videoSecurityLabel.setText(text); if (!videoSecurityLabel.isVisible()) videoSecurityLabel.setVisible(true); } + else if (visible) + videoSecurityLabel.setVisible(visible); revalidate(); repaint(); @@ -740,7 +760,7 @@ public void run() configService.setProperty(zidAorKey, callPeer.getAddress()); /* * Reduce length of ZID name to fit the security status label. - * The security status label's tooltip contains the full name + * The security status label's tool tip contains the full name * including an explanatory text. */ String label = zidNameValue; diff --git a/src/net/java/sip/communicator/impl/gui/main/call/conference/ConferencePeerPanel.java b/src/net/java/sip/communicator/impl/gui/main/call/conference/ConferencePeerPanel.java index c55b8d250..7866c9793 100644 --- a/src/net/java/sip/communicator/impl/gui/main/call/conference/ConferencePeerPanel.java +++ b/src/net/java/sip/communicator/impl/gui/main/call/conference/ConferencePeerPanel.java @@ -169,7 +169,7 @@ public ConferencePeerPanel( + "DISABLE_SOUND_LEVEL_INDICATORS", false)) { - call.addLocalUserSoundLevelListener(soundLevelListener); + call.addLocalUserSoundLevelListener(soundLevelListener); } } diff --git a/src/net/java/sip/communicator/impl/gui/main/call/conference/VideoConferenceCallPanel.java b/src/net/java/sip/communicator/impl/gui/main/call/conference/VideoConferenceCallPanel.java index eeb374427..28047d96e 100644 --- a/src/net/java/sip/communicator/impl/gui/main/call/conference/VideoConferenceCallPanel.java +++ b/src/net/java/sip/communicator/impl/gui/main/call/conference/VideoConferenceCallPanel.java @@ -40,6 +40,14 @@ public class VideoConferenceCallPanel private static final Logger logger = Logger.getLogger(VideoConferenceCallPanel.class); + /** + * The compile-time flag which indicates whether each video displayed by + * VideoConferenceCallPanel is to be depicted with an associated + * tool bar showing information and controls related to the (local or + * remote) peer sending the respective video. + */ + private static final boolean SHOW_TOOLBARS = true; + /** * The facility which aids this instance with the video-related information. */ @@ -193,6 +201,7 @@ private Component createDefaultPhotoPanel(ImageIcon photoLabelIcon) photoLabel.setIcon(photoLabelIcon); + @SuppressWarnings("serial") JPanel photoPanel = new TransparentPanel(new GridBagLayout()) { @@ -711,17 +720,24 @@ protected ConferenceCallPeerRenderer updateViewFromModel( /** * {@inheritDoc} * - * Temporarily disables the use of ConferenceParticipantContainer - * because the functionality implemented in the model at the time of this - * writing does not fully support mapping of visual Components + * If {@link #SHOW_TOOLBARS} is false, disables the use of + * ConferenceParticipantContainer. A reason for such a value of + * SHOW_TOOLBARS may be that the functionality implemented in the + * model may not fully support mapping of visual Components * displaying video to telephony conference participants (e.g. in telephony - * conferences utilizing the Jitsi VideoBridge server-side technology). - * Instead displays the videos only, does not map videos to participants and - * does not display participants who do not have videos. + * conferences utilizing the Jitsi VideoBridge server-side technology). In + * such a case displays the videos only, does not map videos to participants + * and does not display participants who do not have videos. */ @Override protected void updateViewFromModelInEventDispatchThread() { + if (SHOW_TOOLBARS) + { + super.updateViewFromModelInEventDispatchThread(); + return; + } + /* * Determine the set of visual Components displaying video streaming * between the local peer/user and the remote peers which are to be diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerMediaHandlerJabberImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerMediaHandlerJabberImpl.java index eaa5563c9..73f0856dc 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerMediaHandlerJabberImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/CallPeerMediaHandlerJabberImpl.java @@ -6,8 +6,10 @@ */ package net.java.sip.communicator.impl.protocol.jabber; +import java.awt.*; import java.lang.reflect.*; import java.util.*; +import java.util.List; import net.java.sip.communicator.impl.protocol.jabber.extensions.cobri.*; import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; @@ -39,16 +41,33 @@ public class CallPeerMediaHandlerJabberImpl = Logger.getLogger(CallPeerMediaHandlerJabberImpl.class); /** - * The TransportManager implementation handling our address - * management. - */ - private TransportManagerJabberImpl transportManager; - - /** - * The Object which is used for synchronization (e.g. wait - * and notify) related to {@link #transportManager}. + * Determines whether a specific XMPP feature is supported by both a + * specific ScServiceDiscoveryManager (may be referred to as the + * local peer) and a specific DiscoverInfo (may be thought of as + * the remote peer). + * + * @param discoveryManager the ScServiceDiscoveryManager to be + * checked whether it includes the specified feature + * @param discoverInfo the DiscoveryInfo which is to be checked + * whether it contains the specified feature. If discoverInfo is + * null, it is considered to contain the specified feature. + * @param feature the feature to be determined whether it is supported by + * both the specified discoveryManager and the specified + * discoverInfo + * @return true if the specified feature is supported by + * both the specified discoveryManager and the specified + * discoverInfo; otherwise, false */ - private final Object transportManagerSyncRoot = new Object(); + private static boolean isFeatureSupported( + ScServiceDiscoveryManager discoveryManager, + DiscoverInfo discoverInfo, + String feature) + { + return + discoveryManager.includesFeature(feature) + && ((discoverInfo == null) + || discoverInfo.containsFeature(feature)); + } /** * The current description of the streams that we have going toward the @@ -58,6 +77,11 @@ public class CallPeerMediaHandlerJabberImpl private final Map localContentMap = new LinkedHashMap(); + /** + * The QualityControl of this CallPeerMediaHandler. + */ + private final QualityControlWrapper qualityControls; + /** * The current description of the streams that the remote side has with us. * We use {@link LinkedHashMap}s to make sure that we preserve @@ -78,9 +102,16 @@ public class CallPeerMediaHandlerJabberImpl private boolean supportQualityControls = false; /** - * The QualityControl of this CallPeerMediaHandler. + * The TransportManager implementation handling our address + * management. */ - private final QualityControlWrapper qualityControls; + private TransportManagerJabberImpl transportManager; + + /** + * The Object which is used for synchronization (e.g. wait + * and notify) related to {@link #transportManager}. + */ + private final Object transportManagerSyncRoot = new Object(); /** * Creates a new handler that will be managing media streams for @@ -97,373 +128,374 @@ public CallPeerMediaHandlerJabberImpl(CallPeerJabberImpl peer) } /** - * Lets the underlying implementation take note of this error and only - * then throws it to the using bundles. + * Determines and sets the direction that a stream, which has been place on + * hold by the remote party, would need to go back to after being + * re-activated. If the stream is not currently on hold (i.e. it is still + * sending media), this method simply returns its current direction. * - * @param message the message to be logged and then wrapped in a new - * OperationFailedException - * @param errorCode the error code to be assigned to the new - * OperationFailedException - * @param cause the Throwable that has caused the necessity to log - * an error and have a new OperationFailedException thrown + * @param stream the {@link MediaStreamTarget} whose post-hold direction + * we'd like to determine. * - * @throws OperationFailedException the exception that we wanted this method - * to throw. + * @return the {@link MediaDirection} that we need to set on stream + * once it is reactivate. */ - protected void throwOperationFailedException( - String message, - int errorCode, - Throwable cause) - throws OperationFailedException + private MediaDirection calculatePostHoldDirection(MediaStream stream) { - ProtocolProviderServiceJabberImpl.throwOperationFailedException( - message, - errorCode, - cause, - logger); + MediaDirection streamDirection = stream.getDirection(); + + if (streamDirection.allowsSending()) + return streamDirection; + + /* + * When calculating a direction we need to take into account 1) what + * direction the remote party had asked for before putting us on hold, + * 2) what the user preference is for the stream's media type, 3) our + * local hold status, 4) the direction supported by the device this + * stream is reading from. + */ + + // 1. what the remote party originally told us (from our perspective) + ContentPacketExtension content = remoteContentMap.get(stream.getName()); + MediaDirection postHoldDir + = JingleUtils.getDirection(content, !getPeer().isInitiator()); + + // 2. the user preference + MediaDevice device = stream.getDevice(); + + postHoldDir + = postHoldDir.and( + getDirectionUserPreference(device.getMediaType())); + + // 3. our local hold status + if (isLocallyOnHold()) + postHoldDir = postHoldDir.and(MediaDirection.SENDONLY); + + // 4. the device direction + postHoldDir = postHoldDir.and(device.getDirection()); + + stream.setDirection(postHoldDir); + + return postHoldDir; } /** - * Get the remote content of a specific content type (like audio or video). - * - * @param contentType content type name - * @return remote ContentPacketExtension or null if not found + * Closes the CallPeerMediaHandler. */ - public ContentPacketExtension getRemoteContent(String contentType) + @Override + public synchronized void close() { - for(String key : remoteContentMap.keySet()) - { - ContentPacketExtension content = remoteContentMap.get(key); - RtpDescriptionPacketExtension description - = JingleUtils.getRtpDescription(content); + super.close(); - if(description.getMedia().equals(contentType)) - return content; - } - return null; + OperationSetDesktopSharingClientJabberImpl client + = (OperationSetDesktopSharingClientJabberImpl) + getPeer().getProtocolProvider().getOperationSet( + OperationSetDesktopSharingClient.class); + + if (client != null) + client.fireRemoteControlRevoked(getPeer()); } /** - * {@inheritDoc} + * Creates a {@link ContentPacketExtension}s of the streams for a + * specific MediaDevice. * - * In the case of a telephony conference organized by the local peer/user - * via the Jitsi VideoBridge server-side technology, returns an SSRC - * reported by the server as received on the channel allocated by the local - * peer/user for the purposes of communicating with the CallPeer - * associated with this instance. + * @param dev MediaDevice + * @return the {@link ContentPacketExtension}s of stream that this + * handler is prepared to initiate. + * @throws OperationFailedException if we fail to create the descriptions + * for reasons like problems with device interaction, allocating ports, etc. */ - @Override - public long getRemoteSSRC(MediaType mediaType) + private ContentPacketExtension createContent(MediaDevice dev) + throws OperationFailedException { + MediaType mediaType = dev.getMediaType(); + MediaDirection direction = dev.getDirection(); + /* - * If the Jitsi VideoBridge server-side technology is utilized, a single - * MediaStream (per MediaType) is shared among the participating - * CallPeers and, consequently, the remote SSRCs cannot be associated - * with the CallPeers from which they are actually being sent. That's - * why the server will report them to the conference focus. + * In the case of RTP translation performed by the conference focus, + * the conference focus is not required to capture media. */ - TransportManagerJabberImpl transportManager = this.transportManager; + if (!(MediaType.VIDEO.equals(mediaType) + && isRTPTranslationEnabled(mediaType))) + direction = direction.and(getDirectionUserPreference(mediaType)); + if (isLocallyOnHold()) + direction = direction.and(MediaDirection.SENDONLY); - if (transportManager instanceof RawUdpTransportManager) - { - RawUdpTransportManager rawUdpTransportManager - = (RawUdpTransportManager) transportManager; - CobriConferenceIQ.Channel channel - = rawUdpTransportManager.getCobriChannel( - mediaType, - false /* remote */); + QualityPreset sendQualityPreset = null; + QualityPreset receiveQualityPreset = null; - if (channel != null) - { - long[] ssrcs = channel.getSSRCs(); + if(qualityControls != null) + { + // the one we will send is the one the remote has announced as + // receive + sendQualityPreset = qualityControls.getRemoteReceivePreset(); + // the one we want to receive is the one the remote can send + receiveQualityPreset = qualityControls.getRemoteSendMaxPreset(); + } + if(direction != MediaDirection.INACTIVE) + { + ContentPacketExtension content + = createContentForOffer( + getLocallySupportedFormats(dev, + sendQualityPreset, + receiveQualityPreset), + direction, + dev.getSupportedExtensions()); + RtpDescriptionPacketExtension description + = JingleUtils.getRtpDescription(content); - /* - * A peer (regardless of whether it is local or remote) may send - * multiple RTP streams at any time. In such a case, it is not - * clear which one of their SSRCs is to be returned. Anyway, the - * super says that the returned is the last known and we will - * presume that the last known in the list reported by the Jitsi - * VideoBridge server is the last. - */ - for (int i = ssrcs.length - 1; i >= 0; i--) - { - long remoteSSRC = ssrcs[i]; + //SDES + // It is important to set SDES before ZRTP in order to make GTALK + // application able to work with SDES. + setSDesEncryptionToDescription(mediaType, description, null); + //ZRTP + setZrtpEncryptionToDescription(mediaType, description, null); - if (remoteSSRC != -1) - return remoteSSRC; - } - } + return content; } - - return super.getRemoteSSRC(mediaType); + return null; } /** - * Get the local content of a specific content type (like audio or video). + * Creates a {@link ContentPacketExtension} for a particular stream. * - * @param contentType content type name - * @return remote ContentPacketExtension or null if not found + * @param mediaType MediaType of the content + * @return a {@link ContentPacketExtension} + * @throws OperationFailedException if we fail to create the descriptions + * for reasons like - problems with device interaction, allocating ports, + * etc. */ - public ContentPacketExtension getLocalContent(String contentType) + public ContentPacketExtension createContentForMedia(MediaType mediaType) + throws OperationFailedException { - for(String key : localContentMap.keySet()) - { - ContentPacketExtension content = localContentMap.get(key); - RtpDescriptionPacketExtension description - = JingleUtils.getRtpDescription(content); + MediaDevice dev = getDefaultDevice(mediaType); - if(description.getMedia().equals(contentType)) - return content; - } + if(isDeviceActive(dev)) + return createContent(dev); return null; } /** - * Creates if necessary, and configures the stream that this - * MediaHandler is using for the MediaType matching the - * one of the MediaDevice. This method extends the one already - * available by adding a stream name, corresponding to a stream's content - * name. + * Generates an Jingle {@link ContentPacketExtension} for the specified + * {@link MediaFormat} list, direction and RTP extensions taking account + * the local streaming preference for the corresponding media type. * - * @param streamName the name of the stream as indicated in the XMPP - * content element. - * @param connector the MediaConnector that we'd like to bind the - * newly created stream to. - * @param device the MediaDevice that we'd like to attach the newly - * created MediaStream to. - * @param format the MediaFormat that we'd like the new - * MediaStream to be set to transmit in. - * @param target the MediaStreamTarget containing the RTP and RTCP - * address:port couples that the new stream would be sending packets to. - * @param direction the MediaDirection that we'd like the new - * stream to use (i.e. sendonly, sendrecv, recvonly, or inactive). - * @param rtpExtensions the list of RTPExtensions that should be - * enabled for this stream. - * @param masterStream whether the stream to be used as master if secured - * - * @return the newly created MediaStream. + * @param supportedFormats the list of MediaFormats that we'd + * like to advertise. + * @param direction the MediaDirection that we'd like to establish + * the stream in. + * @param supportedExtensions the list of RTPExtensions that we'd + * like to advertise in the MediaDescription. * - * @throws OperationFailedException if creating the stream fails for any - * reason (like for example accessing the device or setting the format). + * @return a newly created {@link ContentPacketExtension} representing + * streams that we'd be able to handle. */ - protected MediaStream initStream(String streamName, - StreamConnector connector, - MediaDevice device, - MediaFormat format, - MediaStreamTarget target, - MediaDirection direction, - List rtpExtensions, - boolean masterStream) - throws OperationFailedException + private ContentPacketExtension createContentForOffer( + List supportedFormats, + MediaDirection direction, + List supportedExtensions) { - MediaStream stream - = super.initStream( - connector, - device, - format, - target, - direction, - rtpExtensions, - masterStream); - - if (stream != null) - stream.setName(streamName); + ContentPacketExtension content + = JingleUtils.createDescription( + ContentPacketExtension.CreatorEnum.initiator, + supportedFormats.get(0).getMediaType().toString(), + JingleUtils.getSenders(direction, !getPeer().isInitiator()), + supportedFormats, + supportedExtensions, + getDynamicPayloadTypes(), + getRtpExtensionsRegistry()); - return stream; + this.localContentMap.put(content.getName(), content); + return content; } /** - * Parses and handles the specified offer and returns a content - * extension representing the current state of this media handler. This - * method MUST only be called when offer is the first session - * description that this MediaHandler is seeing. + * Creates a List containing the {@link ContentPacketExtension}s of + * the streams that this handler is prepared to initiate depending on + * available MediaDevices and local on-hold and video transmission + * preferences. * - * @param offer the offer that we'd like to parse, handle and get an answer - * for. + * @return a {@link List} containing the {@link ContentPacketExtension}s of + * streams that this handler is prepared to initiate. * - * @throws OperationFailedException if we have a problem satisfying the - * description received in offer (e.g. failed to open a device or - * initialize a stream ...). - * @throws IllegalArgumentException if there's a problem with - * offer's format or semantics. + * @throws OperationFailedException if we fail to create the descriptions + * for reasons like problems with device interaction, allocating ports, etc. */ - public void processOffer(List offer) - throws OperationFailedException, - IllegalArgumentException + public List createContentList() + throws OperationFailedException { - // prepare to generate answers to all the incoming descriptions - List answer - = new ArrayList(offer.size()); - boolean atLeastOneValidDescription = false; + // Describe the media. + List mediaDescs + = new ArrayList(); - for (ContentPacketExtension content : offer) + for (MediaType mediaType : MediaType.values()) { - remoteContentMap.put(content.getName(), content); - - RtpDescriptionPacketExtension description - = JingleUtils.getRtpDescription(content); - MediaType mediaType - = MediaType.parseString( description.getMedia() ); - - List remoteFormats - = JingleUtils.extractFormats( - description, - getDynamicPayloadTypes()); - MediaDevice dev = getDefaultDevice(mediaType); - MediaDirection devDirection - = (dev == null) ? MediaDirection.INACTIVE : dev.getDirection(); - - // Take the preference of the user with respect to streaming - // mediaType into account. - devDirection - = devDirection.and(getDirectionUserPreference(mediaType)); - - // determine the direction that we need to announce. - MediaDirection remoteDirection = JingleUtils.getDirection( - content, getPeer().isInitiator()); - MediaDirection direction - = devDirection.getDirectionForAnswer(remoteDirection); + if (isDeviceActive(dev)) + { + MediaDirection direction = dev.getDirection(); - // intersect the MediaFormats of our device with remote ones - List mutuallySupportedFormats - = intersectFormats(remoteFormats, getLocallySupportedFormats(dev)); - - // check whether we will be exchanging any RTP extensions. - List offeredRTPExtensions - = JingleUtils.extractRTPExtensions( - description, this.getRtpExtensionsRegistry()); + /* + * In the case of RTP translation performed by the conference + * focus, the conference focus is not required to capture media. + */ + if (!(MediaType.VIDEO.equals(mediaType) + && isRTPTranslationEnabled(mediaType))) + { + direction + = direction.and(getDirectionUserPreference(mediaType)); + } + if (isLocallyOnHold()) + direction = direction.and(MediaDirection.SENDONLY); - List supportedExtensions - = getExtensionsForType(mediaType); + /* + * If we're only able to receive, we don't have to offer it at + * all. For example, we have to offer audio and no video when we + * start an audio call. + */ + if (MediaDirection.RECVONLY.equals(direction)) + direction = MediaDirection.INACTIVE; - List rtpExtensions = intersectRTPExtensions( - offeredRTPExtensions, supportedExtensions); + if (direction != MediaDirection.INACTIVE) + { + ContentPacketExtension content + = createContentForOffer( + getLocallySupportedFormats(dev), + direction, + dev.getSupportedExtensions()); + RtpDescriptionPacketExtension description + = JingleUtils.getRtpDescription(content); - // transport - /* - * RawUdpTransportPacketExtension extends - * IceUdpTransportPacketExtension so getting - * IceUdpTransportPacketExtension should suffice. - */ - IceUdpTransportPacketExtension transport - = content.getFirstChildOfType( - IceUdpTransportPacketExtension.class); + //SDES + // It is important to set SDES before ZRTP in order to make + // GTALK application able to work with SDES. + setSDesEncryptionToDescription( + mediaType, + description, + null); + //ZRTP + setZrtpEncryptionToDescription( + mediaType, + description, + null); - // stream target - MediaStreamTarget target = null; + // we request a desktop sharing session so add the inputevt + // extension in the "video" content + if (description.getMedia().equals( + MediaType.VIDEO.toString()) + && getLocalInputEvtAware()) + { + content.addChildExtension( + new InputEvtPacketExtension()); + } - try - { - target = JingleUtils.extractDefaultTarget(content); - } - catch(IllegalArgumentException e) - { - logger.warn("Fail to extract default target", e); + mediaDescs.add(content); + } } + } - // according to XEP-176, transport element in session-initiate - // "MAY instead be empty (with each candidate to be sent as the - // payload of a transport-info message)". - int targetDataPort - = (target == null && transport != null) - ? -1 - : (target != null) ? target.getDataAddress().getPort() : 0; - - /* - * TODO If the offered transport is not supported, attempt to fall - * back to a supported one using transport-replace. - */ - setTransportManager(transport.getNamespace()); + // Fail if no media is described (e.g. all devices are inactive). + if (mediaDescs.isEmpty()) + { + ProtocolProviderServiceJabberImpl.throwOperationFailedException( + "We couldn't find any active Audio/Video devices" + + " and couldn't create a call", + OperationFailedException.GENERAL_ERROR, + null, + logger); + } - if (mutuallySupportedFormats.isEmpty() - || (devDirection == MediaDirection.INACTIVE) - || (targetDataPort == 0)) - { - // skip stream and continue. contrary to sip we don't seem to - // need to send per-stream disabling answer and only one at the - // end. + // Describe the transport(s). + TransportInfoSender transportInfoSender + = getTransportManager().getXmlNamespace().equals( + ProtocolProviderServiceJabberImpl.URN_GOOGLE_TRANSPORT_P2P) + ? new TransportInfoSender() + { + public void sendTransportInfo( + Iterable contents) + { + getPeer().sendTransportInfo(contents); + } + } + : null; - //close the stream in case it already exists - closeStream(mediaType); - continue; - } + return harvestCandidates(null, mediaDescs, transportInfoSender); + } - // create the answer description - ContentPacketExtension ourContent - = JingleUtils.createDescription( - content.getCreator(), - content.getName(), - JingleUtils.getSenders( - direction, - !getPeer().isInitiator()), - mutuallySupportedFormats, - rtpExtensions, - getDynamicPayloadTypes(), - getRtpExtensionsRegistry()); - - RtpDescriptionPacketExtension localDescription = - JingleUtils.getRtpDescription(ourContent); - - // Sets ZRTP or SDES, depending on the preferences for this account. - setAndAddPreferredEncryptionProtocol( - mediaType, - localDescription, - description); - - // Got a content which has inputevt. It means that the peer requests - // a desktop sharing session so tell it we support inputevt. - if(content.getChildExtensionsOfType(InputEvtPacketExtension.class) - != null) - { - ourContent.addChildExtension(new InputEvtPacketExtension()); - } + /** + * Creates a List containing the {@link ContentPacketExtension}s of + * the streams of a specific MediaType that this handler is + * prepared to initiate depending on available MediaDevices and + * local on-hold and video transmission preferences. + * + * @param mediaType MediaType of the content + * @return a {@link List} containing the {@link ContentPacketExtension}s of + * streams that this handler is prepared to initiate. + * + * @throws OperationFailedException if we fail to create the descriptions + * for reasons like - problems with device interaction, allocating ports, + * etc. + */ + public List createContentList(MediaType mediaType) + throws OperationFailedException + { + MediaDevice dev = getDefaultDevice(mediaType); + List mediaDescs + = new ArrayList(); - answer.add(ourContent); - localContentMap.put(content.getName(), ourContent); + if (isDeviceActive(dev)) + { + ContentPacketExtension content = createContent(dev); - atLeastOneValidDescription = true; + if (content != null) + mediaDescs.add(content); } - if (!atLeastOneValidDescription) + // Fail if no media is described (e.g. all devices are inactive). + if (mediaDescs.isEmpty()) { ProtocolProviderServiceJabberImpl.throwOperationFailedException( - "Offer contained no media formats" - + " or no valid media descriptions.", - OperationFailedException.ILLEGAL_ARGUMENT, + "We couldn't find any active Audio/Video devices and " + + "couldn't create a call", + OperationFailedException.GENERAL_ERROR, null, logger); } - /* - * In order to minimize post-pickup delay, start establishing the - * connectivity prior to ringing. - */ - harvestCandidates( - offer, - answer, - new TransportInfoSender() - { - public void sendTransportInfo( - Iterable contents) + // Describe the transport(s). + TransportInfoSender transportInfoSender + = getTransportManager().getXmlNamespace().equals( + ProtocolProviderServiceJabberImpl.URN_GOOGLE_TRANSPORT_P2P) + ? new TransportInfoSender() { - getPeer().sendTransportInfo(contents); + public void sendTransportInfo( + Iterable contents) + { + getPeer().sendTransportInfo(contents); + } } - }); + : null; - /* - * While it may sound like we can completely eliminate the post-pickup - * delay by waiting for the connectivity establishment to finish, it may - * not be possible in all cases. We are the Jingle session responder so, - * in the case of the ICE UDP transport, we are not the controlling ICE - * Agent and we cannot be sure when the controlling ICE Agent will - * perform the nomination. It could, for example, choose to wait for our - * session-accept to perform the nomination which will deadlock us if we - * have chosen to wait for the connectivity establishment to finish - * before we begin ringing and send session-accept. - */ - getTransportManager().startConnectivityEstablishment(offer); + return harvestCandidates(null, mediaDescs, transportInfoSender); + } + + /** + * Overrides to give access to the transport manager to send events + * about ICE state changes. + * + * @param property the name of the property of this + * PropertyChangeNotifier which had its value changed + * @param oldValue the value of the property with the specified name before + * the change + * @param newValue the value of the property with the specified name after + */ + @Override + protected void firePropertyChange( + String property, + Object oldValue, Object newValue) + { + super.firePropertyChange(property, oldValue, newValue); } /** @@ -618,653 +650,493 @@ public Iterable generateSessionAccept() } /** - * Creates a {@link ContentPacketExtension}s of the streams for a - * specific MediaDevice. + * Get the local content of a specific content type (like audio or video). * - * @param dev MediaDevice - * @return the {@link ContentPacketExtension}s of stream that this - * handler is prepared to initiate. - * @throws OperationFailedException if we fail to create the descriptions - * for reasons like problems with device interaction, allocating ports, etc. + * @param contentType content type name + * @return remote ContentPacketExtension or null if not found */ - private ContentPacketExtension createContent(MediaDevice dev) - throws OperationFailedException + public ContentPacketExtension getLocalContent(String contentType) { - MediaType mediaType = dev.getMediaType(); - MediaDirection direction = dev.getDirection(); - - /* - * In the case of RTP translation performed by the conference focus, - * the conference focus is not required to capture media. - */ - if (!(MediaType.VIDEO.equals(mediaType) - && isRTPTranslationEnabled(mediaType))) - direction = direction.and(getDirectionUserPreference(mediaType)); - if (isLocallyOnHold()) - direction = direction.and(MediaDirection.SENDONLY); - - QualityPreset sendQualityPreset = null; - QualityPreset receiveQualityPreset = null; - - if(qualityControls != null) - { - // the one we will send is the one the remote has announced as - // receive - sendQualityPreset = qualityControls.getRemoteReceivePreset(); - // the one we want to receive is the one the remote can send - receiveQualityPreset = qualityControls.getRemoteSendMaxPreset(); - } - if(direction != MediaDirection.INACTIVE) + for(String key : localContentMap.keySet()) { - ContentPacketExtension content - = createContentForOffer( - getLocallySupportedFormats(dev, - sendQualityPreset, - receiveQualityPreset), - direction, - dev.getSupportedExtensions()); + ContentPacketExtension content = localContentMap.get(key); RtpDescriptionPacketExtension description = JingleUtils.getRtpDescription(content); - //SDES - // It is important to set SDES before ZRTP in order to make GTALK - // application able to work with SDES. - setSDesEncryptionToDescription(mediaType, description, null); - //ZRTP - setZrtpEncryptionToDescription(mediaType, description, null); - - return content; + if(description.getMedia().equals(contentType)) + return content; } return null; } /** - * Creates a {@link ContentPacketExtension} for a particular stream. + * Returns the quality control for video calls if any. * - * @param mediaType MediaType of the content - * @return a {@link ContentPacketExtension} - * @throws OperationFailedException if we fail to create the descriptions - * for reasons like - problems with device interaction, allocating ports, - * etc. + * @return the implemented quality control. */ - public ContentPacketExtension createContentForMedia(MediaType mediaType) - throws OperationFailedException + public QualityControl getQualityControl() { - MediaDevice dev = getDefaultDevice(mediaType); + if(supportQualityControls) + { + return qualityControls; + } + else + { + // we have detected that its not supported and return null + // and control ui won't be visible + return null; + } + } - if(isDeviceActive(dev)) - return createContent(dev); + /** + * Get the remote content of a specific content type (like audio or video). + * + * @param contentType content type name + * @return remote ContentPacketExtension or null if not found + */ + public ContentPacketExtension getRemoteContent(String contentType) + { + for(String key : remoteContentMap.keySet()) + { + ContentPacketExtension content = remoteContentMap.get(key); + RtpDescriptionPacketExtension description + = JingleUtils.getRtpDescription(content); + + if(description.getMedia().equals(contentType)) + return content; + } return null; } /** - * Creates a List containing the {@link ContentPacketExtension}s of - * the streams of a specific MediaType that this handler is - * prepared to initiate depending on available MediaDevices and - * local on-hold and video transmission preferences. - * - * @param mediaType MediaType of the content - * @return a {@link List} containing the {@link ContentPacketExtension}s of - * streams that this handler is prepared to initiate. + * {@inheritDoc} * - * @throws OperationFailedException if we fail to create the descriptions - * for reasons like - problems with device interaction, allocating ports, - * etc. + * In the case of a telephony conference organized by the local peer/user + * via the Jitsi VideoBridge server-side technology, returns an SSRC + * reported by the server as received on the channel allocated by the local + * peer/user for the purposes of communicating with the CallPeer + * associated with this instance. */ - public List createContentList(MediaType mediaType) - throws OperationFailedException + @Override + public long getRemoteSSRC(MediaType mediaType) { - MediaDevice dev = getDefaultDevice(mediaType); - List mediaDescs - = new ArrayList(); + long[] ssrcs = getRemoteSSRCs(mediaType); - if (isDeviceActive(dev)) + /* + * A peer (regardless of whether it is local or remote) may send + * multiple RTP streams at any time. In such a case, it is not clear + * which one of their SSRCs is to be returned. Anyway, the super says + * that the returned is the last known. We will presume that the last + * known in the list reported by the Jitsi VideoBridge server is the + * last. + */ + for (int i = ssrcs.length - 1; i >= 0; i--) { - ContentPacketExtension content = createContent(dev); + long ssrc = ssrcs[i]; - if (content != null) - mediaDescs.add(content); + if (ssrc != -1) + return ssrc; } - // Fail if no media is described (e.g. all devices are inactive). - if (mediaDescs.isEmpty()) - { - ProtocolProviderServiceJabberImpl.throwOperationFailedException( - "We couldn't find any active Audio/Video devices and " - + "couldn't create a call", - OperationFailedException.GENERAL_ERROR, - null, - logger); - } - - // Describe the transport(s). - TransportInfoSender transportInfoSender - = getTransportManager().getXmlNamespace().equals( - ProtocolProviderServiceJabberImpl.URN_GOOGLE_TRANSPORT_P2P) - ? new TransportInfoSender() - { - public void sendTransportInfo( - Iterable contents) - { - getPeer().sendTransportInfo(contents); - } - } - : null; - - return harvestCandidates(null, mediaDescs, transportInfoSender); - } + return super.getRemoteSSRC(mediaType); + } /** - * Creates a List containing the {@link ContentPacketExtension}s of - * the streams that this handler is prepared to initiate depending on - * available MediaDevices and local on-hold and video transmission - * preferences. - * - * @return a {@link List} containing the {@link ContentPacketExtension}s of - * streams that this handler is prepared to initiate. + * Gets the SSRCs of RTP streams with a specific MediaType known to + * be received by a MediaStream associated with this instance. + *

+ * Warning: The method may return only one of the many possible + * remote SSRCs in the case of no utilization of the Jitsi VideoBridge + * server-side technology because the super implementation does not + * currently provide support for keeping track of multiple remote SSRCs. + *

* - * @throws OperationFailedException if we fail to create the descriptions - * for reasons like problems with device interaction, allocating ports, etc. + * @param mediaType the MediaType of the RTP streams the SSRCs of + * which are to be returned + * @return an array of long values which represent the SSRCs of RTP + * streams with the specified mediaType known to be received by a + * MediaStream associated with this instance */ - public List createContentList() - throws OperationFailedException + public long[] getRemoteSSRCs(MediaType mediaType) { - // Describe the media. - List mediaDescs - = new ArrayList(); + /* + * If the Jitsi VideoBridge server-side technology is utilized, a single + * MediaStream (per MediaType) is shared among the participating + * CallPeers and, consequently, the remote SSRCs cannot be associated + * with the CallPeers from which they are actually being sent. That's + * why the server will report them to the conference focus. + */ + TransportManagerJabberImpl transportManager = this.transportManager; - for (MediaType mediaType : MediaType.values()) + if (transportManager instanceof RawUdpTransportManager) { - MediaDevice dev = getDefaultDevice(mediaType); - - if (isDeviceActive(dev)) - { - MediaDirection direction = dev.getDirection(); - - /* - * In the case of RTP translation performed by the conference - * focus, the conference focus is not required to capture media. - */ - if (!(MediaType.VIDEO.equals(mediaType) - && isRTPTranslationEnabled(mediaType))) - { - direction - = direction.and(getDirectionUserPreference(mediaType)); - } - if (isLocallyOnHold()) - direction = direction.and(MediaDirection.SENDONLY); - - /* - * If we're only able to receive, we don't have to offer it at - * all. For example, we have to offer audio and no video when we - * start an audio call. - */ - if (MediaDirection.RECVONLY.equals(direction)) - direction = MediaDirection.INACTIVE; - - if (direction != MediaDirection.INACTIVE) - { - ContentPacketExtension content - = createContentForOffer( - getLocallySupportedFormats(dev), - direction, - dev.getSupportedExtensions()); - RtpDescriptionPacketExtension description - = JingleUtils.getRtpDescription(content); - - //SDES - // It is important to set SDES before ZRTP in order to make - // GTALK application able to work with SDES. - setSDesEncryptionToDescription( - mediaType, - description, - null); - //ZRTP - setZrtpEncryptionToDescription( - mediaType, - description, - null); - - // we request a desktop sharing session so add the inputevt - // extension in the "video" content - if (description.getMedia().equals( - MediaType.VIDEO.toString()) - && getLocalInputEvtAware()) - { - content.addChildExtension( - new InputEvtPacketExtension()); - } - - mediaDescs.add(content); - } - } - } + RawUdpTransportManager rawUdpTransportManager + = (RawUdpTransportManager) transportManager; + CobriConferenceIQ.Channel channel + = rawUdpTransportManager.getCobriChannel( + mediaType, + false /* remote */); - // Fail if no media is described (e.g. all devices are inactive). - if (mediaDescs.isEmpty()) - { - ProtocolProviderServiceJabberImpl.throwOperationFailedException( - "We couldn't find any active Audio/Video devices" - + " and couldn't create a call", - OperationFailedException.GENERAL_ERROR, - null, - logger); + if (channel != null) + return channel.getSSRCs(); } - // Describe the transport(s). - TransportInfoSender transportInfoSender - = getTransportManager().getXmlNamespace().equals( - ProtocolProviderServiceJabberImpl.URN_GOOGLE_TRANSPORT_P2P) - ? new TransportInfoSender() - { - public void sendTransportInfo( - Iterable contents) - { - getPeer().sendTransportInfo(contents); - } - } - : null; - - return harvestCandidates(null, mediaDescs, transportInfoSender); - } - - /** - * Generates an Jingle {@link ContentPacketExtension} for the specified - * {@link MediaFormat} list, direction and RTP extensions taking account - * the local streaming preference for the corresponding media type. - * - * @param supportedFormats the list of MediaFormats that we'd - * like to advertise. - * @param direction the MediaDirection that we'd like to establish - * the stream in. - * @param supportedExtensions the list of RTPExtensions that we'd - * like to advertise in the MediaDescription. - * - * @return a newly created {@link ContentPacketExtension} representing - * streams that we'd be able to handle. - */ - private ContentPacketExtension createContentForOffer( - List supportedFormats, - MediaDirection direction, - List supportedExtensions) - { - ContentPacketExtension content - = JingleUtils.createDescription( - ContentPacketExtension.CreatorEnum.initiator, - supportedFormats.get(0).getMediaType().toString(), - JingleUtils.getSenders(direction, !getPeer().isInitiator()), - supportedFormats, - supportedExtensions, - getDynamicPayloadTypes(), - getRtpExtensionsRegistry()); + /* + * XXX The fallback to the super implementation that follows may lead to + * unexpected behavior due to the lack of ability to keep track of + * multiple remote SSRCs. + */ + long ssrc = super.getRemoteSSRC(mediaType); - this.localContentMap.put(content.getName(), content); - return content; + return (ssrc == -1) ? CobriConferenceIQ.NO_SSRCS : new long[] { ssrc }; } /** - * Reinitialize all media contents. + * Gets the TransportManager implementation handling our address + * management. * - * @throws OperationFailedException if we fail to handle content - * for reasons like failing to initialize media devices or streams. - * @throws IllegalArgumentException if there's a problem with the syntax or - * the semantics of content. Method is synchronized in order to - * avoid closing mediaHandler when we are currently in process of - * initializing, configuring and starting streams and anybody interested - * in this operation can synchronize to the mediaHandler instance to wait - * processing to stop (method setState in CallPeer). + * @return the TransportManager implementation handling our address + * management + * @see CallPeerMediaHandler#getTransportManager() */ - public void reinitAllContents() - throws OperationFailedException, - IllegalArgumentException + protected synchronized TransportManagerJabberImpl getTransportManager() { - boolean masterStreamSet = false; - for(String key : remoteContentMap.keySet()) + if (transportManager == null) { - ContentPacketExtension ext = remoteContentMap.get(key); + CallPeerJabberImpl peer = getPeer(); - boolean masterStream = false; - // if we have more than one stream, lets the audio be the master - if(!masterStreamSet) + if (peer.isInitiator()) { - RtpDescriptionPacketExtension description - = JingleUtils.getRtpDescription(ext); - MediaType mediaType - = MediaType.parseString( description.getMedia() ); - - if(remoteContentMap.size() > 1) + synchronized(transportManagerSyncRoot) { - if(mediaType.equals(MediaType.AUDIO)) + try + { + transportManagerSyncRoot.wait(5000); + } + catch(InterruptedException e) { - masterStream = true; - masterStreamSet = true; } } + + if(transportManager == null) + throw new IllegalStateException( + "The initiator is expected to specify the transport" + + " in their offer."); else - { - masterStream = true; - masterStreamSet = true; - } + return transportManager; } + else + { + ProtocolProviderServiceJabberImpl protocolProvider + = peer.getProtocolProvider(); + ScServiceDiscoveryManager discoveryManager + = protocolProvider.getDiscoveryManager(); + DiscoverInfo peerDiscoverInfo = peer.getDiscoverInfo(); - if(ext != null) - processContent(ext, false, masterStream); - } - } + // We use Google P2P transport if both conditions are satisfied: + // - both peers have Google P2P transport in their features; + // - at least one peer is a Gmail or Google Apps account. + // + // Otherwise we go for an ICE-UDP transport + boolean google + = protocolProvider.isGmailOrGoogleAppsAccount() + || ProtocolProviderServiceJabberImpl + .isGmailOrGoogleAppsAccount( + StringUtils.parseServer(peer.getAddress())); - /** - * Reinitialize a media content such as video. - * - * @param name name of the Jingle content - * @param content media content - * @param modify if it correspond to a content-modify for resolution change - * @throws OperationFailedException if we fail to handle content - * for reasons like failing to initialize media devices or streams. - * @throws IllegalArgumentException if there's a problem with the syntax or - * the semantics of content. Method is synchronized in order to - * avoid closing mediaHandler when we are currently in process of - * initializing, configuring and starting streams and anybody interested - * in this operation can synchronize to the mediaHandler instance to wait - * processing to stop (method setState in CallPeer). - */ - public void reinitContent( - String name, - ContentPacketExtension content, - boolean modify) - throws OperationFailedException, - IllegalArgumentException - { - ContentPacketExtension ext = remoteContentMap.get(name); + // Put Google P2P transport first. We will take it + // for a node that support both ICE-UDP and Google P2P to use + // Google relay. + if (google + && isFeatureSupported( + discoveryManager, + peerDiscoverInfo, + ProtocolProviderServiceJabberImpl + .URN_GOOGLE_TRANSPORT_P2P)) + { + transportManager = new P2PTransportManager(peer); + } + else + { + /* + * The list of possible transports ordered by decreasing + * preference. + */ + String[] transports + = new String[] + { + ProtocolProviderServiceJabberImpl + .URN_XMPP_JINGLE_ICE_UDP_1, + ProtocolProviderServiceJabberImpl + .URN_XMPP_JINGLE_RAW_UDP_0 + }; - if(ext != null) - { - if(modify) - { - processContent(content, modify, false); - remoteContentMap.put(name, content); - } - else - { - ext.setSenders(content.getSenders()); - processContent(ext, modify, false); - remoteContentMap.put(name, ext); - } - } - } + /* + * If the local peer is a conference focus and there is a + * Jitsi VideoBridge working on the server, prefer a + * transport which will route the conference through there. + */ + CallJabberImpl call = peer.getCall(); - /** - * Removes a media content with a specific name from the session represented - * by this CallPeerMediaHandlerJabberImpl and closes its associated - * media stream. - * - * @param name the name of the media content to be removed from this session - */ - public void removeContent(String name) - { - removeContent(localContentMap, name); - removeContent(remoteContentMap, name); - getTransportManager().removeContent(name); - } + if (call.getConference().isJitsiVideoBridge()) + { + String jitsiVideoBridge + = protocolProvider.getJitsiVideoBridge(); - /** - * Removes a media content with a specific name from the session represented - * by this CallPeerMediaHandlerJabberImpl and closes its associated - * media stream. - * - * @param contentMap the Map in which the specified name - * has an association with the media content to be removed - * @param name the name of the media content to be removed from this session - */ - private void removeContent( - Map contentMap, - String name) - { - ContentPacketExtension content = contentMap.remove(name); + if ((jitsiVideoBridge != null) + && (jitsiVideoBridge.length() != 0)) + { + /* + * Make the transport of the Jitsi VideoBridge the + * most preferred. + */ + String jitsiVideoBridgeTransport + = ProtocolProviderServiceJabberImpl + .URN_XMPP_JINGLE_RAW_UDP_0; - if (content != null) - { - RtpDescriptionPacketExtension description - = JingleUtils.getRtpDescription(content); - String media = description.getMedia(); + for (int i = 1; i < transports.length; i++) + { + if (jitsiVideoBridgeTransport.equals( + transports[i])) + { + transports[i] = transports[0]; + transports[0] = jitsiVideoBridgeTransport; + } + } - if (media != null) - closeStream(MediaType.parseString(media)); + /* + * XXX If a telephony conference utilizing the Jitsi + * VideoBridge server-side telephony conferencing + * technology is to work, its only chance of working + * is to use RAW UDP at the time of this writing. + */ + transports + = new String[] { jitsiVideoBridgeTransport }; + } + } + + /* + * Select the first transport from the list of possible + * transports ordered by decreasing preference which is + * supported by the local and the remote peers. + */ + for (String transport : transports) + { + if (isFeatureSupported( + discoveryManager, + peerDiscoverInfo, + transport)) + { + if (ProtocolProviderServiceJabberImpl + .URN_XMPP_JINGLE_ICE_UDP_1 + .equals(transport)) + { + transportManager + = new IceUdpTransportManager(peer); + } + else if (ProtocolProviderServiceJabberImpl + .URN_XMPP_JINGLE_RAW_UDP_0 + .equals(transport)) + { + transportManager + = new RawUdpTransportManager(peer); + } + + if (transportManager != null) + break; + } + } + + if ((transportManager == null) && logger.isDebugEnabled()) + { + logger.debug( + "No known Jingle transport supported" + + " by Jabber call peer " + + peer); + } + } + } } + return transportManager; } /** - * Notifies this instance that a specific CobriConferenceIQ has - * been received. This CallPeerMediaHandler uses the part of the - * information provided in the specified conferenceIQ which - * concerns it only. + * {@inheritDoc} * - * @param conferenceIQ the CobriConferenceIQ which has been - * received + * In the case of utilization of the Jitsi VideoBridge server-side + * technology, returns the visual Components which display RTP + * video streams reported by the server to be sent by the remote peer + * represented by this instance. */ - void processCobriConferenceIQ(CobriConferenceIQ conferenceIQ) + @Override + public List getVisualComponents() { /* - * This CallPeerMediaHandler stores the media information but it does - * not store the cobri Channels (which contain both media and transport - * information). The TransportManager associated with this instance - * stores the cobri Channels but does not store media information (such - * as the remote SSRCs). An design/implementation choice has to be made - * though and the present one is to have this CallPeerMediaHandler - * transparently (with respect to the TransportManager) store the media - * information inside the TransportManager. + * TODO The super is currently unable to provide the complete set of + * remote SSRCs (i.e. in the case of no utilization of the Jitsi + * VideoBridge server-side technology) so we have to explicitly check + * for Jitsi VideoBridge instead of just relying on the implementation + * of the getRemoteSSRCs(MediaType) method to abstract away that detail. */ - TransportManagerJabberImpl transportManager = this.transportManager; + CallJabberImpl call; + MediaAwareCallConference conference; - if (transportManager instanceof RawUdpTransportManager) + if (((call = getPeer().getCall()) != null) + && ((conference = call.getConference()) != null) + && conference.isJitsiVideoBridge()) { - RawUdpTransportManager rawUdpTransportManager - = (RawUdpTransportManager) transportManager; - long oldAudioRemoteSSRC = getRemoteSSRC(MediaType.AUDIO); - long oldVideoRemoteSSRC = getRemoteSSRC(MediaType.VIDEO); + MediaStream stream = getStream(MediaType.VIDEO); - for (MediaType mediaType : MediaType.values()) + if (stream == null) + return Collections.emptyList(); + else { - CobriConferenceIQ.Channel dst - = rawUdpTransportManager.getCobriChannel( - mediaType, - false /* remote */); + long[] remoteSSRCs = getRemoteSSRCs(MediaType.VIDEO); - if (dst != null) + if (remoteSSRCs.length == 0) + return Collections.emptyList(); + else { - CobriConferenceIQ.Content content - = conferenceIQ.getContent(mediaType.toString()); + VideoMediaStream videoStream = (VideoMediaStream) stream; + List visualComponents + = new LinkedList(); - if (content != null) + for (long remoteSSRC : remoteSSRCs) { - CobriConferenceIQ.Channel src - = content.getChannel(dst.getID()); - - if (src != null) + if (remoteSSRC != -1) { - long[] ssrcs = src.getSSRCs(); + Component visualComponent + = videoStream.getVisualComponent(remoteSSRC); - if (!Arrays.equals(dst.getSSRCs(), ssrcs)) - dst.setSSRCs(src.getSSRCs()); + if (visualComponent != null) + visualComponents.add(visualComponent); } } + return visualComponents; } } - - /* - * Do fire new PropertyChangeEvents for the properties - * AUDIO_REMOTE_SSRC and VIDEO_REMOTE_SSRC if necessary. - */ - long newAudioRemoteSSRC = getRemoteSSRC(MediaType.AUDIO); - long newVideoRemoteSSRC = getRemoteSSRC(MediaType.VIDEO); - - if (oldAudioRemoteSSRC != newAudioRemoteSSRC) - { - firePropertyChange( - AUDIO_REMOTE_SSRC, - oldAudioRemoteSSRC, newAudioRemoteSSRC); - } - if (oldVideoRemoteSSRC != newVideoRemoteSSRC) - { - firePropertyChange( - VIDEO_REMOTE_SSRC, - oldVideoRemoteSSRC, newVideoRemoteSSRC); - } } + + return super.getVisualComponents(); } /** - * Process a ContentPacketExtension and initialize its - * corresponding MediaStream. + * Gathers local candidate addresses. * - * @param content a ContentPacketExtension - * @param modify if it correspond to a content-modify for resolution change - * @param masterStream whether the stream to be used as master - * @throws OperationFailedException if we fail to handle content - * for reasons like failing to initialize media devices or streams. - * @throws IllegalArgumentException if there's a problem with the syntax or - * the semantics of content. The method is synchronized in order to - * avoid closing mediaHandler when we are currently in process of - * initializing, configuring and starting streams and anybody interested - * in this operation can synchronize to the mediaHandler instance to wait - * processing to stop (method setState in CallPeer). - */ - private void processContent( - ContentPacketExtension content, - boolean modify, - boolean masterStream) - throws OperationFailedException, - IllegalArgumentException - { - RtpDescriptionPacketExtension description - = JingleUtils.getRtpDescription(content); - MediaType mediaType - = MediaType.parseString(description.getMedia()); + * @param remote the media descriptions received from the remote peer if any + * or null if local represents an offer from the local + * peer to be sent to the remote peer + * @param local the media descriptions sent or to be sent from the local + * peer to the remote peer. If remote is null, + * local represents an offer from the local peer to be sent to the + * remote peer + * @param transportInfoSender the TransportInfoSender to be used by + * this TransportManagerJabberImpl to send transport-info + * JingleIQs from the local peer to the remote peer if this + * TransportManagerJabberImpl wishes to utilize + * transport-info + * @return the media descriptions of the local peer after the local + * candidate addresses have been gathered as returned by + * {@link TransportManagerJabberImpl#wrapupCandidateHarvest()} + * @throws OperationFailedException if anything goes wrong while starting or + * wrapping up the gathering of local candidate addresses + */ + private List harvestCandidates( + List remote, + List local, + TransportInfoSender transportInfoSender) + throws OperationFailedException + { + long startCandidateHarvestTime = System.currentTimeMillis(); - //stream target TransportManagerJabberImpl transportManager = getTransportManager(); - MediaStreamTarget target = transportManager.getStreamTarget(mediaType); - - if (target == null) - target = JingleUtils.extractDefaultTarget(content); - - // no target port - try next media description - if((target == null) || (target.getDataAddress().getPort() == 0)) - { - closeStream(mediaType); - return; - } - - List supportedFormats = JingleUtils.extractFormats( - description, getDynamicPayloadTypes()); - - MediaDevice dev = getDefaultDevice(mediaType); - if(!isDeviceActive(dev)) + if (remote == null) { - closeStream(mediaType); - return; - } - - MediaDirection devDirection - = (dev == null) ? MediaDirection.INACTIVE : dev.getDirection(); - - // Take the preference of the user with respect to streaming - // mediaType into account. - devDirection - = devDirection.and(getDirectionUserPreference(mediaType)); + /* + * We'll be harvesting candidates in order to make an offer so it + * doesn't make sense to send them in transport-info. + */ + if (!ProtocolProviderServiceJabberImpl.URN_GOOGLE_TRANSPORT_P2P + .equals(transportManager.getXmlNamespace()) + && (transportInfoSender != null)) + throw new IllegalArgumentException("transportInfoSender"); - if (supportedFormats.isEmpty()) - { - //remote party must have messed up our Jingle description. - //throw an exception. - ProtocolProviderServiceJabberImpl.throwOperationFailedException( - "Remote party sent an invalid Jingle answer.", - OperationFailedException.ILLEGAL_ARGUMENT, - null, - logger); + transportManager.startCandidateHarvest(local, transportInfoSender); } - - addZRTPAdvertisedEncryptions(true, description, mediaType); - addSDESAdvertisedEncryptions(true, description, mediaType); - - StreamConnector connector - = transportManager.getStreamConnector(mediaType); - - //determine the direction that we need to announce. - MediaDirection remoteDirection - = JingleUtils.getDirection(content, getPeer().isInitiator()); - MediaDirection direction - = devDirection.getDirectionForAnswer(remoteDirection); - - // update the RTP extensions that we will be exchanging. - List remoteRTPExtensions - = JingleUtils.extractRTPExtensions( - description, getRtpExtensionsRegistry()); - List supportedExtensions - = getExtensionsForType(mediaType); - List rtpExtensions - = intersectRTPExtensions(remoteRTPExtensions, supportedExtensions); - - Map adv - = supportedFormats.get(0).getAdvancedAttributes(); - - if(adv != null) + else { - for(Map.Entry f : adv.entrySet()) - { - if(f.getKey().equals("imageattr")) - supportQualityControls = true; - } + transportManager.startCandidateHarvest( + remote, + local, + transportInfoSender); } - // check for options from remote party and set them locally - if(mediaType.equals(MediaType.VIDEO) && modify) - { - QualityPreset sendQualityPreset = null; - QualityPreset receiveQualityPreset = null; - - // update stream - MediaStream stream = getStream(MediaType.VIDEO); - - if(stream != null && dev != null) - { - List fmts = supportedFormats; - - if(fmts.size() > 0) - { - MediaFormat fmt = fmts.get(0); - - ((VideoMediaStream)stream).updateQualityControl( - fmt.getAdvancedAttributes()); - } - } + long stopCandidateHarvestTime = System.currentTimeMillis(); + long candidateHarvestTime + = stopCandidateHarvestTime - startCandidateHarvestTime; + if (logger.isInfoEnabled()) + logger.info("End candidate harvest within " + + candidateHarvestTime + " ms"); + /* + * TODO Ideally, we wouldn't wrap up that quickly. We need to revisit + * this. + */ + return transportManager.wrapupCandidateHarvest(); + } - if(qualityControls != null) - { - receiveQualityPreset = qualityControls.getRemoteReceivePreset(); - sendQualityPreset = qualityControls.getRemoteSendMaxPreset(); + /** + * Creates if necessary, and configures the stream that this + * MediaHandler is using for the MediaType matching the + * one of the MediaDevice. This method extends the one already + * available by adding a stream name, corresponding to a stream's content + * name. + * + * @param streamName the name of the stream as indicated in the XMPP + * content element. + * @param connector the MediaConnector that we'd like to bind the + * newly created stream to. + * @param device the MediaDevice that we'd like to attach the newly + * created MediaStream to. + * @param format the MediaFormat that we'd like the new + * MediaStream to be set to transmit in. + * @param target the MediaStreamTarget containing the RTP and RTCP + * address:port couples that the new stream would be sending packets to. + * @param direction the MediaDirection that we'd like the new + * stream to use (i.e. sendonly, sendrecv, recvonly, or inactive). + * @param rtpExtensions the list of RTPExtensions that should be + * enabled for this stream. + * @param masterStream whether the stream to be used as master if secured + * + * @return the newly created MediaStream. + * + * @throws OperationFailedException if creating the stream fails for any + * reason (like for example accessing the device or setting the format). + */ + protected MediaStream initStream(String streamName, + StreamConnector connector, + MediaDevice device, + MediaFormat format, + MediaStreamTarget target, + MediaDirection direction, + List rtpExtensions, + boolean masterStream) + throws OperationFailedException + { + MediaStream stream + = super.initStream( + connector, + device, + format, + target, + direction, + rtpExtensions, + masterStream); - supportedFormats - = (dev == null) - ? null - : intersectFormats( - supportedFormats, - getLocallySupportedFormats(dev, - sendQualityPreset, - receiveQualityPreset)); - } - } + if (stream != null) + stream.setName(streamName); - // create the corresponding stream... - initStream( - content.getName(), - connector, - dev, - supportedFormats.get(0), - target, - direction, - rtpExtensions, - masterStream); + return stream; } /** @@ -1326,264 +1198,584 @@ public void processAnswer(List answer) } /** - * Gets the TransportManager implementation handling our address - * management. + * Notifies this instance that a specific CobriConferenceIQ has + * been received. This CallPeerMediaHandler uses the part of the + * information provided in the specified conferenceIQ which + * concerns it only. * - * @return the TransportManager implementation handling our address - * management - * @see CallPeerMediaHandler#getTransportManager() + * @param conferenceIQ the CobriConferenceIQ which has been + * received */ - protected synchronized TransportManagerJabberImpl getTransportManager() + void processCobriConferenceIQ(CobriConferenceIQ conferenceIQ) { - if (transportManager == null) + /* + * This CallPeerMediaHandler stores the media information but it does + * not store the cobri Channels (which contain both media and transport + * information). The TransportManager associated with this instance + * stores the cobri Channels but does not store media information (such + * as the remote SSRCs). An design/implementation choice has to be made + * though and the present one is to have this CallPeerMediaHandler + * transparently (with respect to the TransportManager) store the media + * information inside the TransportManager. + */ + TransportManagerJabberImpl transportManager = this.transportManager; + + if (transportManager instanceof RawUdpTransportManager) { - CallPeerJabberImpl peer = getPeer(); + RawUdpTransportManager rawUdpTransportManager + = (RawUdpTransportManager) transportManager; + long oldAudioRemoteSSRC = getRemoteSSRC(MediaType.AUDIO); + long oldVideoRemoteSSRC = getRemoteSSRC(MediaType.VIDEO); - if (peer.isInitiator()) + for (MediaType mediaType : MediaType.values()) { - synchronized(transportManagerSyncRoot) - { - try - { - transportManagerSyncRoot.wait(5000); - } - catch(InterruptedException e) + CobriConferenceIQ.Channel dst + = rawUdpTransportManager.getCobriChannel( + mediaType, + false /* remote */); + + if (dst != null) + { + CobriConferenceIQ.Content content + = conferenceIQ.getContent(mediaType.toString()); + + if (content != null) { + CobriConferenceIQ.Channel src + = content.getChannel(dst.getID()); + + if (src != null) + { + long[] ssrcs = src.getSSRCs(); + + if (!Arrays.equals(dst.getSSRCs(), ssrcs)) + dst.setSSRCs(src.getSSRCs()); + } } } + } - if(transportManager == null) - throw new IllegalStateException( - "The initiator is expected to specify the transport" - + " in their offer."); - else - return transportManager; + /* + * Do fire new PropertyChangeEvents for the properties + * AUDIO_REMOTE_SSRC and VIDEO_REMOTE_SSRC if necessary. + */ + long newAudioRemoteSSRC = getRemoteSSRC(MediaType.AUDIO); + long newVideoRemoteSSRC = getRemoteSSRC(MediaType.VIDEO); + + if (oldAudioRemoteSSRC != newAudioRemoteSSRC) + { + firePropertyChange( + AUDIO_REMOTE_SSRC, + oldAudioRemoteSSRC, newAudioRemoteSSRC); } - else + if (oldVideoRemoteSSRC != newVideoRemoteSSRC) { - ProtocolProviderServiceJabberImpl protocolProvider - = peer.getProtocolProvider(); - ScServiceDiscoveryManager discoveryManager - = protocolProvider.getDiscoveryManager(); - DiscoverInfo peerDiscoverInfo = peer.getDiscoverInfo(); + firePropertyChange( + VIDEO_REMOTE_SSRC, + oldVideoRemoteSSRC, newVideoRemoteSSRC); + } + } + } - // We use Google P2P transport if both conditions are satisfied: - // - both peers have Google P2P transport in their features; - // - at least one peer is a Gmail or Google Apps account. - // - // Otherwise we go for an ICE-UDP transport - boolean google - = protocolProvider.isGmailOrGoogleAppsAccount() - || ProtocolProviderServiceJabberImpl - .isGmailOrGoogleAppsAccount( - StringUtils.parseServer(peer.getAddress())); + /** + * Process a ContentPacketExtension and initialize its + * corresponding MediaStream. + * + * @param content a ContentPacketExtension + * @param modify if it correspond to a content-modify for resolution change + * @param masterStream whether the stream to be used as master + * @throws OperationFailedException if we fail to handle content + * for reasons like failing to initialize media devices or streams. + * @throws IllegalArgumentException if there's a problem with the syntax or + * the semantics of content. The method is synchronized in order to + * avoid closing mediaHandler when we are currently in process of + * initializing, configuring and starting streams and anybody interested + * in this operation can synchronize to the mediaHandler instance to wait + * processing to stop (method setState in CallPeer). + */ + private void processContent( + ContentPacketExtension content, + boolean modify, + boolean masterStream) + throws OperationFailedException, + IllegalArgumentException + { + RtpDescriptionPacketExtension description + = JingleUtils.getRtpDescription(content); + MediaType mediaType + = MediaType.parseString(description.getMedia()); - // Put Google P2P transport first. We will take it - // for a node that support both ICE-UDP and Google P2P to use - // Google relay. - if (google - && isFeatureSupported( - discoveryManager, - peerDiscoverInfo, - ProtocolProviderServiceJabberImpl - .URN_GOOGLE_TRANSPORT_P2P)) + //stream target + TransportManagerJabberImpl transportManager = getTransportManager(); + MediaStreamTarget target = transportManager.getStreamTarget(mediaType); + + if (target == null) + target = JingleUtils.extractDefaultTarget(content); + + // no target port - try next media description + if((target == null) || (target.getDataAddress().getPort() == 0)) + { + closeStream(mediaType); + return; + } + + List supportedFormats = JingleUtils.extractFormats( + description, getDynamicPayloadTypes()); + + MediaDevice dev = getDefaultDevice(mediaType); + + if(!isDeviceActive(dev)) + { + closeStream(mediaType); + return; + } + + MediaDirection devDirection + = (dev == null) ? MediaDirection.INACTIVE : dev.getDirection(); + + // Take the preference of the user with respect to streaming + // mediaType into account. + devDirection + = devDirection.and(getDirectionUserPreference(mediaType)); + + if (supportedFormats.isEmpty()) + { + //remote party must have messed up our Jingle description. + //throw an exception. + ProtocolProviderServiceJabberImpl.throwOperationFailedException( + "Remote party sent an invalid Jingle answer.", + OperationFailedException.ILLEGAL_ARGUMENT, + null, + logger); + } + + addZRTPAdvertisedEncryptions(true, description, mediaType); + addSDESAdvertisedEncryptions(true, description, mediaType); + + StreamConnector connector + = transportManager.getStreamConnector(mediaType); + + //determine the direction that we need to announce. + MediaDirection remoteDirection + = JingleUtils.getDirection(content, getPeer().isInitiator()); + MediaDirection direction + = devDirection.getDirectionForAnswer(remoteDirection); + + // update the RTP extensions that we will be exchanging. + List remoteRTPExtensions + = JingleUtils.extractRTPExtensions( + description, getRtpExtensionsRegistry()); + List supportedExtensions + = getExtensionsForType(mediaType); + List rtpExtensions + = intersectRTPExtensions(remoteRTPExtensions, supportedExtensions); + + Map adv + = supportedFormats.get(0).getAdvancedAttributes(); + + if(adv != null) + { + for(Map.Entry f : adv.entrySet()) + { + if(f.getKey().equals("imageattr")) + supportQualityControls = true; + } + } + + // check for options from remote party and set them locally + if(mediaType.equals(MediaType.VIDEO) && modify) + { + QualityPreset sendQualityPreset = null; + QualityPreset receiveQualityPreset = null; + + // update stream + MediaStream stream = getStream(MediaType.VIDEO); + + if(stream != null && dev != null) + { + List fmts = supportedFormats; + + if(fmts.size() > 0) { - transportManager = new P2PTransportManager(peer); + MediaFormat fmt = fmts.get(0); + + ((VideoMediaStream)stream).updateQualityControl( + fmt.getAdvancedAttributes()); } - else - { - /* - * The list of possible transports ordered by decreasing - * preference. - */ - String[] transports - = new String[] - { - ProtocolProviderServiceJabberImpl - .URN_XMPP_JINGLE_ICE_UDP_1, - ProtocolProviderServiceJabberImpl - .URN_XMPP_JINGLE_RAW_UDP_0 - }; + } - /* - * If the local peer is a conference focus and there is a - * Jitsi VideoBridge working on the server, prefer a - * transport which will route the conference through there. - */ - CallJabberImpl call = peer.getCall(); + if(qualityControls != null) + { + receiveQualityPreset = qualityControls.getRemoteReceivePreset(); + sendQualityPreset = qualityControls.getRemoteSendMaxPreset(); - if (call.getConference().isJitsiVideoBridge()) - { - String jitsiVideoBridge - = protocolProvider.getJitsiVideoBridge(); + supportedFormats + = (dev == null) + ? null + : intersectFormats( + supportedFormats, + getLocallySupportedFormats(dev, + sendQualityPreset, + receiveQualityPreset)); + } + } - if ((jitsiVideoBridge != null) - && (jitsiVideoBridge.length() != 0)) - { - /* - * Make the transport of the Jitsi VideoBridge the - * most preferred. - */ - String jitsiVideoBridgeTransport - = ProtocolProviderServiceJabberImpl - .URN_XMPP_JINGLE_RAW_UDP_0; + // create the corresponding stream... + initStream( + content.getName(), + connector, + dev, + supportedFormats.get(0), + target, + direction, + rtpExtensions, + masterStream); + } - for (int i = 1; i < transports.length; i++) - { - if (jitsiVideoBridgeTransport.equals( - transports[i])) - { - transports[i] = transports[0]; - transports[0] = jitsiVideoBridgeTransport; - } - } + /** + * Parses and handles the specified offer and returns a content + * extension representing the current state of this media handler. This + * method MUST only be called when offer is the first session + * description that this MediaHandler is seeing. + * + * @param offer the offer that we'd like to parse, handle and get an answer + * for. + * + * @throws OperationFailedException if we have a problem satisfying the + * description received in offer (e.g. failed to open a device or + * initialize a stream ...). + * @throws IllegalArgumentException if there's a problem with + * offer's format or semantics. + */ + public void processOffer(List offer) + throws OperationFailedException, + IllegalArgumentException + { + // prepare to generate answers to all the incoming descriptions + List answer + = new ArrayList(offer.size()); + boolean atLeastOneValidDescription = false; - /* - * XXX If a telephony conference utilizing the Jitsi - * VideoBridge server-side telephony conferencing - * technology is to work, its only chance of working - * is to use RAW UDP at the time of this writing. - */ - transports - = new String[] { jitsiVideoBridgeTransport }; - } - } + for (ContentPacketExtension content : offer) + { + remoteContentMap.put(content.getName(), content); + + RtpDescriptionPacketExtension description + = JingleUtils.getRtpDescription(content); + MediaType mediaType + = MediaType.parseString( description.getMedia() ); + + List remoteFormats + = JingleUtils.extractFormats( + description, + getDynamicPayloadTypes()); + + MediaDevice dev = getDefaultDevice(mediaType); + + MediaDirection devDirection + = (dev == null) ? MediaDirection.INACTIVE : dev.getDirection(); + + // Take the preference of the user with respect to streaming + // mediaType into account. + devDirection + = devDirection.and(getDirectionUserPreference(mediaType)); + + // determine the direction that we need to announce. + MediaDirection remoteDirection = JingleUtils.getDirection( + content, getPeer().isInitiator()); + MediaDirection direction + = devDirection.getDirectionForAnswer(remoteDirection); + + // intersect the MediaFormats of our device with remote ones + List mutuallySupportedFormats + = intersectFormats(remoteFormats, getLocallySupportedFormats(dev)); + + // check whether we will be exchanging any RTP extensions. + List offeredRTPExtensions + = JingleUtils.extractRTPExtensions( + description, this.getRtpExtensionsRegistry()); + + List supportedExtensions + = getExtensionsForType(mediaType); + + List rtpExtensions = intersectRTPExtensions( + offeredRTPExtensions, supportedExtensions); + + // transport + /* + * RawUdpTransportPacketExtension extends + * IceUdpTransportPacketExtension so getting + * IceUdpTransportPacketExtension should suffice. + */ + IceUdpTransportPacketExtension transport + = content.getFirstChildOfType( + IceUdpTransportPacketExtension.class); + + // stream target + MediaStreamTarget target = null; + + try + { + target = JingleUtils.extractDefaultTarget(content); + } + catch(IllegalArgumentException e) + { + logger.warn("Fail to extract default target", e); + } + + // according to XEP-176, transport element in session-initiate + // "MAY instead be empty (with each candidate to be sent as the + // payload of a transport-info message)". + int targetDataPort + = (target == null && transport != null) + ? -1 + : (target != null) ? target.getDataAddress().getPort() : 0; + + /* + * TODO If the offered transport is not supported, attempt to fall + * back to a supported one using transport-replace. + */ + setTransportManager(transport.getNamespace()); + + if (mutuallySupportedFormats.isEmpty() + || (devDirection == MediaDirection.INACTIVE) + || (targetDataPort == 0)) + { + // skip stream and continue. contrary to sip we don't seem to + // need to send per-stream disabling answer and only one at the + // end. + + //close the stream in case it already exists + closeStream(mediaType); + continue; + } + + // create the answer description + ContentPacketExtension ourContent + = JingleUtils.createDescription( + content.getCreator(), + content.getName(), + JingleUtils.getSenders( + direction, + !getPeer().isInitiator()), + mutuallySupportedFormats, + rtpExtensions, + getDynamicPayloadTypes(), + getRtpExtensionsRegistry()); + + RtpDescriptionPacketExtension localDescription = + JingleUtils.getRtpDescription(ourContent); + + // Sets ZRTP or SDES, depending on the preferences for this account. + setAndAddPreferredEncryptionProtocol( + mediaType, + localDescription, + description); + + // Got a content which has inputevt. It means that the peer requests + // a desktop sharing session so tell it we support inputevt. + if(content.getChildExtensionsOfType(InputEvtPacketExtension.class) + != null) + { + ourContent.addChildExtension(new InputEvtPacketExtension()); + } + + answer.add(ourContent); + localContentMap.put(content.getName(), ourContent); + + atLeastOneValidDescription = true; + } + + if (!atLeastOneValidDescription) + { + ProtocolProviderServiceJabberImpl.throwOperationFailedException( + "Offer contained no media formats" + + " or no valid media descriptions.", + OperationFailedException.ILLEGAL_ARGUMENT, + null, + logger); + } - /* - * Select the first transport from the list of possible - * transports ordered by decreasing preference which is - * supported by the local and the remote peers. - */ - for (String transport : transports) + /* + * In order to minimize post-pickup delay, start establishing the + * connectivity prior to ringing. + */ + harvestCandidates( + offer, + answer, + new TransportInfoSender() { - if (isFeatureSupported( - discoveryManager, - peerDiscoverInfo, - transport)) + public void sendTransportInfo( + Iterable contents) { - if (ProtocolProviderServiceJabberImpl - .URN_XMPP_JINGLE_ICE_UDP_1 - .equals(transport)) - { - transportManager - = new IceUdpTransportManager(peer); - } - else if (ProtocolProviderServiceJabberImpl - .URN_XMPP_JINGLE_RAW_UDP_0 - .equals(transport)) - { - transportManager - = new RawUdpTransportManager(peer); - } - - if (transportManager != null) - break; + getPeer().sendTransportInfo(contents); } - } + }); - if ((transportManager == null) && logger.isDebugEnabled()) - { - logger.debug( - "No known Jingle transport supported" - + " by Jabber call peer " - + peer); - } - } - } - } - return transportManager; + /* + * While it may sound like we can completely eliminate the post-pickup + * delay by waiting for the connectivity establishment to finish, it may + * not be possible in all cases. We are the Jingle session responder so, + * in the case of the ICE UDP transport, we are not the controlling ICE + * Agent and we cannot be sure when the controlling ICE Agent will + * perform the nomination. It could, for example, choose to wait for our + * session-accept to perform the nomination which will deadlock us if we + * have chosen to wait for the connectivity establishment to finish + * before we begin ringing and send session-accept. + */ + getTransportManager().startConnectivityEstablishment(offer); } /** - * Determines whether a specific XMPP feature is supported by both a - * specific ScServiceDiscoveryManager (may be referred to as the - * local peer) and a specific DiscoverInfo (may be thought of as - * the remote peer). + * Processes the transport-related information provided by the remote + * peer in a specific set of ContentPacketExtensions. * - * @param discoveryManager the ScServiceDiscoveryManager to be - * checked whether it includes the specified feature - * @param discoverInfo the DiscoveryInfo which is to be checked - * whether it contains the specified feature. If discoverInfo is - * null, it is considered to contain the specified feature. - * @param feature the feature to be determined whether it is supported by - * both the specified discoveryManager and the specified - * discoverInfo - * @return true if the specified feature is supported by - * both the specified discoveryManager and the specified - * discoverInfo; otherwise, false + * @param contents the ContentPacketExtenions provided by the + * remote peer and containing the transport-related information to + * be processed + * @throws OperationFailedException if anything goes wrong while processing + * the transport-related information provided by the remote peer in + * the specified set of ContentPacketExtensions */ - private static boolean isFeatureSupported( - ScServiceDiscoveryManager discoveryManager, - DiscoverInfo discoverInfo, - String feature) + public void processTransportInfo(Iterable contents) + throws OperationFailedException { - return - discoveryManager.includesFeature(feature) - && ((discoverInfo == null) - || discoverInfo.containsFeature(feature)); + if (getTransportManager().startConnectivityEstablishment(contents)) + { + //wrapupConnectivityEstablishment(); + } } /** - * Sets the TransportManager implementation to handle our address - * management by Jingle transport XML namespace. + * Reinitialize all media contents. * - * @param xmlns the Jingle transport XML namespace specifying the - * TransportManager implementation type to be set on this instance - * to handle our address management - * @throws IllegalArgumentException if the specified xmlns does not - * specify a (supported) TransportManager implementation type + * @throws OperationFailedException if we fail to handle content + * for reasons like failing to initialize media devices or streams. + * @throws IllegalArgumentException if there's a problem with the syntax or + * the semantics of content. Method is synchronized in order to + * avoid closing mediaHandler when we are currently in process of + * initializing, configuring and starting streams and anybody interested + * in this operation can synchronize to the mediaHandler instance to wait + * processing to stop (method setState in CallPeer). */ - private void setTransportManager(String xmlns) - throws IllegalArgumentException + public void reinitAllContents() + throws OperationFailedException, + IllegalArgumentException { - // Is this really going to be an actual change? - if ((transportManager != null) - && transportManager.getXmlNamespace().equals(xmlns)) + boolean masterStreamSet = false; + for(String key : remoteContentMap.keySet()) { - return; - } + ContentPacketExtension ext = remoteContentMap.get(key); - CallPeerJabberImpl peer = getPeer(); + boolean masterStream = false; + // if we have more than one stream, lets the audio be the master + if(!masterStreamSet) + { + RtpDescriptionPacketExtension description + = JingleUtils.getRtpDescription(ext); + MediaType mediaType + = MediaType.parseString( description.getMedia() ); - if (!peer.getProtocolProvider().getDiscoveryManager().includesFeature( - xmlns)) - { - throw new IllegalArgumentException( - "Unsupported Jingle transport " + xmlns); + if(remoteContentMap.size() > 1) + { + if(mediaType.equals(MediaType.AUDIO)) + { + masterStream = true; + masterStreamSet = true; + } + } + else + { + masterStream = true; + masterStreamSet = true; + } + } + + if(ext != null) + processContent(ext, false, masterStream); } + } - /* - * TODO The transportManager is going to be changed so it may need to be - * disposed prior to the change. - */ + /** + * Reinitialize a media content such as video. + * + * @param name name of the Jingle content + * @param content media content + * @param modify if it correspond to a content-modify for resolution change + * @throws OperationFailedException if we fail to handle content + * for reasons like failing to initialize media devices or streams. + * @throws IllegalArgumentException if there's a problem with the syntax or + * the semantics of content. Method is synchronized in order to + * avoid closing mediaHandler when we are currently in process of + * initializing, configuring and starting streams and anybody interested + * in this operation can synchronize to the mediaHandler instance to wait + * processing to stop (method setState in CallPeer). + */ + public void reinitContent( + String name, + ContentPacketExtension content, + boolean modify) + throws OperationFailedException, + IllegalArgumentException + { + ContentPacketExtension ext = remoteContentMap.get(name); - if (xmlns.equals( - ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_ICE_UDP_1)) - { - transportManager = new IceUdpTransportManager(peer); - } - else if (xmlns.equals( - ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_RAW_UDP_0)) - { - transportManager = new RawUdpTransportManager(peer); - } - else if (xmlns.equals( - ProtocolProviderServiceJabberImpl.URN_GOOGLE_TRANSPORT_P2P)) - { - transportManager = new P2PTransportManager(peer); - } - else + if(ext != null) { - throw new IllegalArgumentException( - "Unsupported Jingle transport " + xmlns); + if(modify) + { + processContent(content, modify, false); + remoteContentMap.put(name, content); + } + else + { + ext.setSenders(content.getSenders()); + processContent(ext, modify, false); + remoteContentMap.put(name, ext); + } } + } - synchronized(transportManagerSyncRoot) + /** + * Removes a media content with a specific name from the session represented + * by this CallPeerMediaHandlerJabberImpl and closes its associated + * media stream. + * + * @param contentMap the Map in which the specified name + * has an association with the media content to be removed + * @param name the name of the media content to be removed from this session + */ + private void removeContent( + Map contentMap, + String name) + { + ContentPacketExtension content = contentMap.remove(name); + + if (content != null) { - transportManagerSyncRoot.notify(); + RtpDescriptionPacketExtension description + = JingleUtils.getRtpDescription(content); + String media = description.getMedia(); + + if (media != null) + closeStream(MediaType.parseString(media)); } } + /** + * Removes a media content with a specific name from the session represented + * by this CallPeerMediaHandlerJabberImpl and closes its associated + * media stream. + * + * @param name the name of the media content to be removed from this session + */ + public void removeContent(String name) + { + removeContent(localContentMap, name); + removeContent(remoteContentMap, name); + getTransportManager().removeContent(name); + } + /** * Acts upon a notification received from the remote party indicating that * they've put us on/off hold. @@ -1622,138 +1814,74 @@ public void setRemotelyOnHold(boolean onHold) } /** - * Determines and sets the direction that a stream, which has been place on - * hold by the remote party, would need to go back to after being - * re-activated. If the stream is not currently on hold (i.e. it is still - * sending media), this method simply returns its current direction. - * - * @param stream the {@link MediaStreamTarget} whose post-hold direction - * we'd like to determine. + * Sometimes as initing a call with custom preset can set and we force + * that quality controls is supported. * - * @return the {@link MediaDirection} that we need to set on stream - * once it is reactivate. + * @param value whether quality controls is supported.. */ - private MediaDirection calculatePostHoldDirection(MediaStream stream) + public void setSupportQualityControls(boolean value) { - MediaDirection streamDirection = stream.getDirection(); - - if (streamDirection.allowsSending()) - return streamDirection; - - /* - * When calculating a direction we need to take into account 1) what - * direction the remote party had asked for before putting us on hold, - * 2) what the user preference is for the stream's media type, 3) our - * local hold status, 4) the direction supported by the device this - * stream is reading from. - */ - - // 1. what the remote party originally told us (from our perspective) - ContentPacketExtension content = remoteContentMap.get(stream.getName()); - MediaDirection postHoldDir - = JingleUtils.getDirection(content, !getPeer().isInitiator()); - - // 2. the user preference - MediaDevice device = stream.getDevice(); - - postHoldDir - = postHoldDir.and( - getDirectionUserPreference(device.getMediaType())); - - // 3. our local hold status - if (isLocallyOnHold()) - postHoldDir = postHoldDir.and(MediaDirection.SENDONLY); - - // 4. the device direction - postHoldDir = postHoldDir.and(device.getDirection()); - - stream.setDirection(postHoldDir); - - return postHoldDir; + this.supportQualityControls = value; } /** - * Gathers local candidate addresses. + * Sets the TransportManager implementation to handle our address + * management by Jingle transport XML namespace. * - * @param remote the media descriptions received from the remote peer if any - * or null if local represents an offer from the local - * peer to be sent to the remote peer - * @param local the media descriptions sent or to be sent from the local - * peer to the remote peer. If remote is null, - * local represents an offer from the local peer to be sent to the - * remote peer - * @param transportInfoSender the TransportInfoSender to be used by - * this TransportManagerJabberImpl to send transport-info - * JingleIQs from the local peer to the remote peer if this - * TransportManagerJabberImpl wishes to utilize - * transport-info - * @return the media descriptions of the local peer after the local - * candidate addresses have been gathered as returned by - * {@link TransportManagerJabberImpl#wrapupCandidateHarvest()} - * @throws OperationFailedException if anything goes wrong while starting or - * wrapping up the gathering of local candidate addresses + * @param xmlns the Jingle transport XML namespace specifying the + * TransportManager implementation type to be set on this instance + * to handle our address management + * @throws IllegalArgumentException if the specified xmlns does not + * specify a (supported) TransportManager implementation type */ - private List harvestCandidates( - List remote, - List local, - TransportInfoSender transportInfoSender) - throws OperationFailedException + private void setTransportManager(String xmlns) + throws IllegalArgumentException { - long startCandidateHarvestTime = System.currentTimeMillis(); + // Is this really going to be an actual change? + if ((transportManager != null) + && transportManager.getXmlNamespace().equals(xmlns)) + { + return; + } - TransportManagerJabberImpl transportManager = getTransportManager(); + CallPeerJabberImpl peer = getPeer(); - if (remote == null) + if (!peer.getProtocolProvider().getDiscoveryManager().includesFeature( + xmlns)) { - /* - * We'll be harvesting candidates in order to make an offer so it - * doesn't make sense to send them in transport-info. - */ - if (!ProtocolProviderServiceJabberImpl.URN_GOOGLE_TRANSPORT_P2P - .equals(transportManager.getXmlNamespace()) - && (transportInfoSender != null)) - throw new IllegalArgumentException("transportInfoSender"); + throw new IllegalArgumentException( + "Unsupported Jingle transport " + xmlns); + } - transportManager.startCandidateHarvest(local, transportInfoSender); + /* + * TODO The transportManager is going to be changed so it may need to be + * disposed prior to the change. + */ + + if (xmlns.equals( + ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_ICE_UDP_1)) + { + transportManager = new IceUdpTransportManager(peer); + } + else if (xmlns.equals( + ProtocolProviderServiceJabberImpl.URN_XMPP_JINGLE_RAW_UDP_0)) + { + transportManager = new RawUdpTransportManager(peer); + } + else if (xmlns.equals( + ProtocolProviderServiceJabberImpl.URN_GOOGLE_TRANSPORT_P2P)) + { + transportManager = new P2PTransportManager(peer); } else { - transportManager.startCandidateHarvest( - remote, - local, - transportInfoSender); + throw new IllegalArgumentException( + "Unsupported Jingle transport " + xmlns); } - long stopCandidateHarvestTime = System.currentTimeMillis(); - long candidateHarvestTime - = stopCandidateHarvestTime - startCandidateHarvestTime; - if (logger.isInfoEnabled()) - logger.info("End candidate harvest within " - + candidateHarvestTime + " ms"); - /* - * TODO Ideally, we wouldn't wrap up that quickly. We need to revisit - * this. - */ - return transportManager.wrapupCandidateHarvest(); - } - - /** - * Processes the transport-related information provided by the remote - * peer in a specific set of ContentPacketExtensions. - * - * @param contents the ContentPacketExtenions provided by the - * remote peer and containing the transport-related information to - * be processed - * @throws OperationFailedException if anything goes wrong while processing - * the transport-related information provided by the remote peer in - * the specified set of ContentPacketExtensions - */ - public void processTransportInfo(Iterable contents) - throws OperationFailedException - { - if (getTransportManager().startConnectivityEstablishment(contents)) + synchronized(transportManagerSyncRoot) { - //wrapupConnectivityEstablishment(); + transportManagerSyncRoot.notify(); } } @@ -1781,6 +1909,33 @@ public void start() super.start(); } + /** + * Lets the underlying implementation take note of this error and only + * then throws it to the using bundles. + * + * @param message the message to be logged and then wrapped in a new + * OperationFailedException + * @param errorCode the error code to be assigned to the new + * OperationFailedException + * @param cause the Throwable that has caused the necessity to log + * an error and have a new OperationFailedException thrown + * + * @throws OperationFailedException the exception that we wanted this method + * to throw. + */ + protected void throwOperationFailedException( + String message, + int errorCode, + Throwable cause) + throws OperationFailedException + { + ProtocolProviderServiceJabberImpl.throwOperationFailedException( + message, + errorCode, + cause, + logger); + } + /** * Notifies the associated TransportManagerJabberImpl that it * should conclude any connectivity establishment, waits for it to actually @@ -1809,69 +1964,4 @@ private void wrapupConnectivityEstablishment() } } } - - /** - * Returns the quality control for video calls if any. - * - * @return the implemented quality control. - */ - public QualityControl getQualityControl() - { - if(supportQualityControls) - { - return qualityControls; - } - else - { - // we have detected that its not supported and return null - // and control ui won't be visible - return null; - } - } - - /** - * Sometimes as initing a call with custom preset can set and we force - * that quality controls is supported. - * - * @param value whether quality controls is supported.. - */ - public void setSupportQualityControls(boolean value) - { - this.supportQualityControls = value; - } - - /** - * Closes the CallPeerMediaHandler. - */ - @Override - public synchronized void close() - { - super.close(); - - OperationSetDesktopSharingClientJabberImpl client - = (OperationSetDesktopSharingClientJabberImpl) - getPeer().getProtocolProvider().getOperationSet( - OperationSetDesktopSharingClient.class); - - if (client != null) - client.fireRemoteControlRevoked(getPeer()); - } - - /** - * Overrides to give access to the transport manager to send events - * about ICE state changes. - * - * @param property the name of the property of this - * PropertyChangeNotifier which had its value changed - * @param oldValue the value of the property with the specified name before - * the change - * @param newValue the value of the property with the specified name after - */ - @Override - protected void firePropertyChange( - String property, - Object oldValue, Object newValue) - { - super.firePropertyChange(property, oldValue, newValue); - } } diff --git a/src/net/java/sip/communicator/service/protocol/media/AbstractOperationSetVideoTelephony.java b/src/net/java/sip/communicator/service/protocol/media/AbstractOperationSetVideoTelephony.java index 6f012e3e5..7e6e7ed06 100644 --- a/src/net/java/sip/communicator/service/protocol/media/AbstractOperationSetVideoTelephony.java +++ b/src/net/java/sip/communicator/service/protocol/media/AbstractOperationSetVideoTelephony.java @@ -142,26 +142,23 @@ public List getVisualComponents(CallPeer peer) * @return the ConferenceMember corresponding to the given * visualComponent. */ - @SuppressWarnings("unchecked") // work with MediaAware* in media package public ConferenceMember getConferenceMember(CallPeer peer, Component visualComponent) { - VideoMediaStream peerVideoStream - = (VideoMediaStream) ((W)peer).getMediaHandler() - .getStream(MediaType.VIDEO); - - if (peerVideoStream == null) - return null; + @SuppressWarnings("unchecked") + W w = (W) peer; + VideoMediaStream videoStream + = (VideoMediaStream) w.getMediaHandler().getStream(MediaType.VIDEO); - for (ConferenceMember member : peer.getConferenceMembers()) + if (videoStream != null) { - Component memberComponent = peerVideoStream - .getVisualComponent(member.getVideoSsrc()); - - if (memberComponent != null - && memberComponent.equals(visualComponent)) + for (ConferenceMember member : peer.getConferenceMembers()) { - return member; + Component memberComponent + = videoStream.getVisualComponent(member.getVideoSsrc()); + + if (visualComponent.equals(memberComponent)) + return member; } } return null;