/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.java.sip.communicator.service.protocol.media; import java.awt.*; import java.beans.*; import java.net.*; import java.util.*; import java.util.List; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.util.*; import org.jitsi.service.neomedia.*; import org.jitsi.service.neomedia.codec.*; import org.jitsi.service.neomedia.control.*; import org.jitsi.service.neomedia.device.*; import org.jitsi.service.neomedia.event.*; import org.jitsi.service.neomedia.format.*; import org.jitsi.util.event.*; /** * A utility class implementing media control code shared between current * telephony implementations. This class is only meant for use by protocol * implementations and should not be accessed by bundles that are simply using * the telephony functionality. * * @param the peer extension class like for example CallPeerSipImpl * or CallPeerJabberImpl * * @author Emil Ivov * @author Lyubomir Marinov * @author Boris Grozev */ public abstract class CallPeerMediaHandler> extends PropertyChangeNotifier { /** * The Logger used by the CallPeerMediaHandler class and * its instances for logging output. */ private static final Logger logger = Logger.getLogger(CallPeerMediaHandler.class); /** * The name of the CallPeerMediaHandler property which specifies * the local SSRC of its audio MediaStream. */ public static final String AUDIO_LOCAL_SSRC = "AUDIO_LOCAL_SSRC"; /** * The name of the CallPeerMediaHandler property which specifies * the remote SSRC of its audio MediaStream. */ public static final String AUDIO_REMOTE_SSRC = "AUDIO_REMOTE_SSRC"; /** * The constant which signals that a SSRC value is unknown. */ public static final long SSRC_UNKNOWN = -1; /** * The name of the CallPeerMediaHandler property which specifies * the local SSRC of its video MediaStream. */ public static final String VIDEO_LOCAL_SSRC = "VIDEO_LOCAL_SSRC"; /** * The name of the CallPeerMediaHandler property which specifies * the remote SSRC of its video MediaStream. */ public static final String VIDEO_REMOTE_SSRC = "VIDEO_REMOTE_SSRC"; /** * List of advertised encryption methods. Indicated before establishing the * call. */ private List advertisedEncryptionMethods = new ArrayList(); /** * Determines whether or not streaming local audio is currently enabled. */ private MediaDirection audioDirectionUserPreference = MediaDirection.SENDRECV; /** * The AudioMediaStream which this instance uses to send and * receive audio. */ private AudioMediaStream audioStream; /** * The PropertyChangeListener which listens to changes in the * values of the properties of the Call of {@link #peer}. */ private final CallPropertyChangeListener callPropertyChangeListener; /** * The listener that our CallPeer registers for CSRC audio level * events. */ private CsrcAudioLevelListener csrcAudioLevelListener; /** * The object that we are using to sync operations on * csrcAudioLevelListener. */ private final Object csrcAudioLevelListenerLock = new Object(); /** * Contains all dynamic payload type mappings that have been made for this * call. */ private final DynamicPayloadTypeRegistry dynamicPayloadTypes = new DynamicPayloadTypeRegistry(); /** * The KeyFrameRequester implemented by this * CallPeerMediaHandler. */ private final KeyFrameControl.KeyFrameRequester keyFrameRequester = new KeyFrameControl.KeyFrameRequester() { public boolean requestKeyFrame() { return CallPeerMediaHandler.this.requestKeyFrame(); } }; /** * Determines whether we have placed the call on hold locally. */ protected boolean locallyOnHold = false; /** * The listener that the CallPeer registered for local user audio * level events. */ private SimpleAudioLevelListener localUserAudioLevelListener; /** * The object that we are using to sync operations on * localAudioLevelListener. */ private final Object localUserAudioLevelListenerLock = new Object(); /** * The state of this instance which may be shared with multiple other * CallPeerMediaHandlers. */ private MediaHandler mediaHandler; /** * The PropertyChangeListener which listens to changes in the * values of the properties of the MediaStreams of this instance. * Since CallPeerMediaHandler wraps around/shares a * MediaHandler, mediaHandlerPropertyChangeListener * actually listens to PropertyChangeEvents fired by the * MediaHandler in question and forwards them as its own. */ private final PropertyChangeListener mediaHandlerPropertyChangeListener = new PropertyChangeListener() { /** * Notifies this PropertyChangeListener that the value of * a specific property of the notifier it is registered with has * changed. * * @param ev a PropertyChangeEvent which describes the * source of the event, the name of the property which has changed * its value and the old and new values of the property * @see PropertyChangeListener#propertyChange(PropertyChangeEvent) */ public void propertyChange(PropertyChangeEvent ev) { mediaHandlerPropertyChange(ev); } }; /** * A reference to the CallPeer instance that this handler is managing media * streams for. */ private final T peer; /** * Contains all RTP extension mappings (those made through the extmap * attribute) that have been bound during this call. */ private final DynamicRTPExtensionsRegistry rtpExtensionsRegistry = new DynamicRTPExtensionsRegistry(); /** * The SrtpListener which is responsible for the SRTP control. Most * often than not, it is the peer itself. */ private final SrtpListener srtpListener; /** * The listener that our CallPeer registered for stream audio * level events. */ private SimpleAudioLevelListener streamAudioLevelListener; /** * The object that we are using to sync operations on * streamAudioLevelListener. */ private final Object streamAudioLevelListenerLock = new Object(); /** * Determines whether or not streaming local video is currently enabled. * Default is RECVONLY. We tried to have INACTIVE at one point but it was * breaking incoming reINVITEs for video calls.. */ private MediaDirection videoDirectionUserPreference = MediaDirection.RECVONLY; /** * The aid which implements the boilerplate related to adding and removing * VideoListeners and firing VideoEvents to them on behalf * of this instance. */ private final VideoNotifierSupport videoNotifierSupport = new VideoNotifierSupport(this, true); /** * The VideoMediaStream which this instance uses to send and * receive video. */ private VideoMediaStream videoStream; /** * Identifier used to group the audio stream and video stream towards * the CallPeer in SDP. */ private String msLabel = UUID.randomUUID().toString(); /** * The VideoListener which listens to the video * MediaStream of this instance for changes in the availability of * visual Components displaying remote video and re-fires them as * originating from this instance. */ private final VideoListener videoStreamVideoListener = new VideoListener() { /** * Notifies this VideoListener about a specific * VideoEvent. Fires a new VideoEvent which has * this CallPeerMediaHandler as its source and carries the * same information as the specified ev i.e. translates the * specified ev into a VideoEvent fired by this * CallPeerMediaHandler. * * @param ev the VideoEvent to notify this * VideoListener about */ private void onVideoEvent(VideoEvent ev) { VideoEvent clone = ev.clone(CallPeerMediaHandler.this); fireVideoEvent(clone); if (clone.isConsumed()) ev.consume(); } public void videoAdded(VideoEvent ev) { onVideoEvent(ev); } public void videoRemoved(VideoEvent ev) { onVideoEvent(ev); } public void videoUpdate(VideoEvent ev) { onVideoEvent(ev); } }; /** * Creates a new handler that will be managing media streams for * peer. * * @param peer the CallPeer instance that we will be managing * media for. * @param srtpListener the object that receives SRTP security events. */ public CallPeerMediaHandler(T peer, SrtpListener srtpListener) { this.peer = peer; this.srtpListener = srtpListener; setMediaHandler(new MediaHandler()); /* * Listen to the call of peer in order to track the user's choice with * respect to the default audio device. */ MediaAwareCall call = this.peer.getCall(); if (call == null) callPropertyChangeListener = null; else { callPropertyChangeListener = new CallPropertyChangeListener(call); call.addPropertyChangeListener(callPropertyChangeListener); } } /** * Adds encryption method to the list of advertised secure methods. * @param encryptionMethod the method to add. */ public void addAdvertisedEncryptionMethod(SrtpControlType encryptionMethod) { if(!advertisedEncryptionMethods.contains(encryptionMethod)) advertisedEncryptionMethods.add(encryptionMethod); } /** * Registers a specific VideoListener with this instance so that it * starts receiving notifications from it about changes in the availability * of visual Components displaying video. * * @param listener the VideoListener to be registered with this * instance and to start receiving notifications from it about changes in * the availability of visual Components displaying video */ public void addVideoListener(VideoListener listener) { videoNotifierSupport.addVideoListener(listener); } /** * Notifies this instance that a value of a specific property of the * Call of {@link #peer} has changed from a specific old value to a * specific new value. * * @param ev a PropertyChangeEvent which specified the property * which had its value changed and the old and new values of that property */ private void callPropertyChange(PropertyChangeEvent ev) { String propertyName = ev.getPropertyName(); boolean callConferenceChange = MediaAwareCall.CONFERENCE.equals(propertyName); if (callConferenceChange || MediaAwareCall.DEFAULT_DEVICE.equals(propertyName)) { MediaAwareCall call = getPeer().getCall(); if (call == null) return; for (MediaType mediaType : MediaType.values()) { MediaStream stream = getStream(mediaType); if (stream == null) continue; // Update the stream device, if necessary. MediaDevice oldDevice = stream.getDevice(); if (oldDevice != null) { /* * DEFAULT_DEVICE signals that the actual/hardware device * has been changed and we will make sure that is the case * in order to avoid unnecessary changes. CONFERENCE signals * that the associated Call has been moved to a new * telephony conference and we have to move its MediaStreams * to the respective mixers. */ MediaDevice oldValue = (!callConferenceChange && (oldDevice instanceof MediaDeviceWrapper)) ? ((MediaDeviceWrapper) oldDevice) .getWrappedDevice() : oldDevice; MediaDevice newDevice = getDefaultDevice(mediaType); MediaDevice newValue = (!callConferenceChange && (newDevice instanceof MediaDeviceWrapper)) ? ((MediaDeviceWrapper) newDevice) .getWrappedDevice() : newDevice; if (oldValue != newValue) stream.setDevice(newDevice); } stream.setRTPTranslator(call.getRTPTranslator(mediaType)); } } } /** * Closes and null-ifies all streams and connectors and readies this media * handler for garbage collection (or reuse). Synchronized if any other * stream operations are in process we won't interrupt them. */ public synchronized void close() { closeStream(MediaType.AUDIO); closeStream(MediaType.VIDEO); locallyOnHold = false; if (callPropertyChangeListener != null) callPropertyChangeListener.removePropertyChangeListener(); setMediaHandler(null); } /** * Closes the MediaStream that this instance uses for a specific * MediaType and prepares it for garbage collection. * * @param mediaType the MediaType that we'd like to stop a stream * for. */ protected void closeStream(MediaType mediaType) { if (logger.isDebugEnabled()) logger.debug("Closing " + mediaType + " stream for " + getPeer()); /* * This CallPeerMediaHandler releases its reference to the MediaStream * it has initialized via #initStream(). */ boolean mediaHandlerCloseStream = false; switch (mediaType) { case AUDIO: if (audioStream != null) { audioStream = null; mediaHandlerCloseStream = true; } break; case VIDEO: if (videoStream != null) { videoStream = null; mediaHandlerCloseStream = true; } break; } if (mediaHandlerCloseStream) mediaHandler.closeStream(this, mediaType); TransportManager transportManager = queryTransportManager(); if (transportManager != null) transportManager.closeStreamConnector(mediaType); } /** * Returns the first RTPExtension in extList that uses * the specified extensionURN or null if extList * did not contain such an extension. * * @param extList the List that we will be looking through. * @param extensionURN the URN of the RTPExtension that we are * looking for. * * @return the first RTPExtension in extList that uses * the specified extensionURN or null if extList * did not contain such an extension. */ private RTPExtension findExtension(List extList, String extensionURN) { for(RTPExtension rtpExt : extList) if (rtpExt.getURI().toASCIIString().equals(extensionURN)) return rtpExt; return null; } /** * Finds a MediaFormat in a specific list of MediaFormats * which matches a specific MediaFormat. * * @param formats the list of MediaFormats to find the specified * matching MediaFormat into * @param format encoding of the MediaFormat to find * @return the MediaFormat from formats which matches * format if such a match exists in formats; otherwise, * null */ protected MediaFormat findMediaFormat( List formats, MediaFormat format) { for(MediaFormat match : formats) { if (match.matches(format)) return match; } return null; } /** * Notifies the VideoListeners registered with this * CallPeerMediaHandler about a specific type of change in the * availability of a specific visual Component depicting video. * * @param type the type of change as defined by VideoEvent in the * availability of the specified visual Component depicting video * @param visualComponent the visual Component depicting video * which has been added or removed in this CallPeerMediaHandler * @param origin {@link VideoEvent#LOCAL} if the origin of the video is * local (e.g. it is being locally captured); {@link VideoEvent#REMOTE} if * the origin of the video is remote (e.g. a remote peer is streaming it) * @return true if this event and, more specifically, the visual * Component it describes have been consumed and should be * considered owned, referenced (which is important because * Components belong to a single Container at a time); * otherwise, false */ protected boolean fireVideoEvent( int type, Component visualComponent, int origin) { return videoNotifierSupport.fireVideoEvent( type, visualComponent, origin, true); } /** * Notifies the VideoListeners registered with this * CallPeerMediaHandler about a specific VideoEvent. * * @param event the VideoEvent to fire to the * VideoListeners registered with this * CallPeerMediaHandler */ public void fireVideoEvent(VideoEvent event) { videoNotifierSupport.fireVideoEvent(event, true); } /** * Returns the advertised methods for securing the call, * this are the methods like SDES, ZRTP that are * indicated in the initial session initialization. Missing here doesn't * mean the other party don't support it. * @return the advertised encryption methods. */ public SrtpControlType[] getAdvertisedEncryptionMethods() { return advertisedEncryptionMethods.toArray( new SrtpControlType[advertisedEncryptionMethods.size()]); } /** * Gets a MediaDevice which is capable of capture and/or playback * of media of the specified MediaType, is the default choice of * the user for a MediaDevice with the specified MediaType * and is appropriate for the current states of the associated * CallPeer and Call. *

* For example, when the local peer is acting as a conference focus in the * Call of the associated CallPeer, the audio device must * be a mixer. *

* * @param mediaType the MediaType in which the retrieved * MediaDevice is to capture and/or play back media * @return a MediaDevice which is capable of capture and/or * playback of media of the specified mediaType, is the default * choice of the user for a MediaDevice with the specified * mediaType and is appropriate for the current states of the * associated CallPeer and Call */ protected MediaDevice getDefaultDevice(MediaType mediaType) { return getPeer().getCall().getDefaultDevice(mediaType); } /** * Gets the MediaDirection value which represents the preference of * the user with respect to streaming media of the specified * MediaType. * * @param mediaType the MediaType to retrieve the user preference * for * @return a MediaDirection value which represents the preference * of the user with respect to streaming media of the specified * mediaType */ protected MediaDirection getDirectionUserPreference(MediaType mediaType) { switch (mediaType) { case AUDIO: return audioDirectionUserPreference; case VIDEO: return videoDirectionUserPreference; case DATA: return MediaDirection.INACTIVE; default: throw new IllegalArgumentException("mediaType"); } } /** * Returns the {@link DynamicPayloadTypeRegistry} instance we are currently * using. * * @return the {@link DynamicPayloadTypeRegistry} instance we are currently * using. */ protected DynamicPayloadTypeRegistry getDynamicPayloadTypes() { return this.dynamicPayloadTypes; } /** * Gets the SRTP control type used for a given media type. * * @param mediaType the MediaType to get the SRTP control type for * @return the SRTP control type (MIKEY, SDES, ZRTP) used for the given * media type or null if SRTP is not enabled for the given media * type */ public SrtpControl getEncryptionMethod(MediaType mediaType) { return mediaHandler.getEncryptionMethod(this, mediaType); } /** * Returns a (possibly empty) List of RTPExtensions * supported by the device that this media handler uses to handle media of * the specified type. * * @param type the MediaType of the device whose * RTPExtensions we are interested in. * * @return a (possibly empty) List of RTPExtensions * supported by the device that this media handler uses to handle media of * the specified type. */ protected List getExtensionsForType(MediaType type) { MediaDevice device = getDefaultDevice(type); return device != null ? device.getSupportedExtensions() : new ArrayList(); } /** * Returns the harvesting time (in ms) for the harvester given in parameter. * * @param harvesterName The class name if the harvester. * * @return The harvesting time (in ms) for the harvester given in parameter. * 0 if this harvester does not exists, if the ICE agent is null, or if the * agent has never harvested with this harvester. */ public long getHarvestingTime(String harvesterName) { TransportManager transportManager = queryTransportManager(); return (transportManager == null) ? null : transportManager.getHarvestingTime(harvesterName); } /** * Returns the extended type of the candidate selected if this transport * manager is using ICE. * * @param streamName The stream name (AUDIO, VIDEO); * * @return The extended type of the candidate selected if this transport * manager is using ICE. Otherwise, returns null. */ public String getICECandidateExtendedType(String streamName) { TransportManager transportManager = queryTransportManager(); return (transportManager == null) ? null : transportManager.getICECandidateExtendedType(streamName); } /** * Returns the ICE local host address. * * @param streamName The stream name (AUDIO, VIDEO); * * @return the ICE local host address if this transport * manager is using ICE. Otherwise, returns null. */ public InetSocketAddress getICELocalHostAddress(String streamName) { TransportManager transportManager = queryTransportManager(); return (transportManager == null) ? null : transportManager.getICELocalHostAddress(streamName); } /** * Returns the ICE local reflexive address (server or peer reflexive). * * @param streamName The stream name (AUDIO, VIDEO); * * @return the ICE local reflexive address. May be null if this transport * manager is not using ICE or if there is no reflexive address for the * local candidate used. */ public InetSocketAddress getICELocalReflexiveAddress(String streamName) { TransportManager transportManager = queryTransportManager(); return (transportManager == null) ? null : transportManager.getICELocalReflexiveAddress(streamName); } /** * Returns the ICE local relayed address (server or peer relayed). * * @param streamName The stream name (AUDIO, VIDEO); * * @return the ICE local relayed address. May be null if this transport * manager is not using ICE or if there is no relayed address for the * local candidate used. */ public InetSocketAddress getICELocalRelayedAddress(String streamName) { TransportManager transportManager = queryTransportManager(); return (transportManager == null) ? null : transportManager.getICELocalRelayedAddress(streamName); } /** * Returns the ICE remote host address. * * @param streamName The stream name (AUDIO, VIDEO); * * @return the ICE remote host address if this transport * manager is using ICE. Otherwise, returns null. */ public InetSocketAddress getICERemoteHostAddress(String streamName) { TransportManager transportManager = queryTransportManager(); return (transportManager == null) ? null : transportManager.getICERemoteHostAddress(streamName); } /** * Returns the ICE remote reflexive address (server or peer reflexive). * * @param streamName The stream name (AUDIO, VIDEO); * * @return the ICE remote reflexive address. May be null if this transport * manager is not using ICE or if there is no reflexive address for the * remote candidate used. */ public InetSocketAddress getICERemoteReflexiveAddress(String streamName) { TransportManager transportManager = queryTransportManager(); return (transportManager == null) ? null : transportManager.getICERemoteReflexiveAddress(streamName); } /** * Returns the ICE remote relayed address (server or peer relayed). * * @param streamName The stream name (AUDIO, VIDEO); * * @return the ICE remote relayed address. May be null if this transport * manager is not using ICE or if there is no relayed address for the * remote candidate used. */ public InetSocketAddress getICERemoteRelayedAddress(String streamName) { TransportManager transportManager = queryTransportManager(); return (transportManager == null) ? null : transportManager.getICERemoteRelayedAddress(streamName); } /** * Returns the current state of ICE processing. * * @return the current state of ICE processing if this transport * manager is using ICE. Otherwise, returns null. */ public String getICEState() { TransportManager transportManager = queryTransportManager(); return (transportManager == null) ? null : transportManager.getICEState(); } /** * Returns a list of locally supported MediaFormats for the * given MediaDevice, ordered in descending priority. Takes into * account the configuration obtained from the ProtocolProvider * instance associated this media handler -- if its set up to override the * global encoding settings, uses that configuration, otherwise uses the * global configuration. * * @param mediaDevice the MediaDevice. * * @return a non-null list of locally supported MediaFormats for * mediaDevice, in decreasing order of priority. * * @see CallPeerMediaHandler#getLocallySupportedFormats(MediaDevice, * QualityPreset, QualityPreset) */ public List getLocallySupportedFormats(MediaDevice mediaDevice) { return getLocallySupportedFormats(mediaDevice, null, null); } /** * Returns a list of locally supported MediaFormats for the * given MediaDevice, ordered in descending priority. Takes into * account the configuration obtained from the ProtocolProvider * instance associated this media handler -- if its set up to override the * global encoding settings, uses that configuration, otherwise uses the * global configuration. * * @param mediaDevice the MediaDevice. * @param sendPreset the preset used to set some of the format parameters, * used for video and settings. * @param receivePreset the preset used to set the receive format * parameters, used for video and settings. * * @return a non-null list of locally supported MediaFormats for * mediaDevice, in decreasing order of priority. */ public List getLocallySupportedFormats( MediaDevice mediaDevice, QualityPreset sendPreset, QualityPreset receivePreset) { if(mediaDevice == null) return Collections.emptyList(); Map accountProperties = getPeer().getProtocolProvider().getAccountID() .getAccountProperties(); String overrideEncodings = accountProperties.get(ProtocolProviderFactory.OVERRIDE_ENCODINGS); if(Boolean.parseBoolean(overrideEncodings)) { /* * The account properties associated with the CallPeer of this * CallPeerMediaHandler override the global EncodingConfiguration. */ EncodingConfiguration encodingConfiguration = ProtocolMediaActivator.getMediaService() .createEmptyEncodingConfiguration(); encodingConfiguration.loadProperties( accountProperties, ProtocolProviderFactory.ENCODING_PROP_PREFIX); return mediaDevice.getSupportedFormats( sendPreset, receivePreset, encodingConfiguration); } else /* The global EncodingConfiguration is in effect. */ { return mediaDevice.getSupportedFormats(sendPreset, receivePreset); } } /** * Gets the visual Component, if any, depicting the video streamed * from the local peer to the remote peer. * * @return the visual Component depicting the local video if local * video is actually being streamed from the local peer to the remote peer; * otherwise, null */ public Component getLocalVisualComponent() { MediaStream videoStream = getStream(MediaType.VIDEO); return ((videoStream == null) || !isLocalVideoTransmissionEnabled()) ? null : ((VideoMediaStream) videoStream).getLocalVisualComponent(); } public MediaHandler getMediaHandler() { return mediaHandler; } /** * Returns the number of harvesting for this agent. * * @return The number of harvesting for this agent. */ public int getNbHarvesting() { TransportManager transportManager = queryTransportManager(); return (transportManager == null) ? null : transportManager.getNbHarvesting(); } /** * Returns the number of harvesting time for the harvester given in * parameter. * * @param harvesterName The class name if the harvester. * * @return The number of harvesting time for the harvester given in * parameter. */ public int getNbHarvesting(String harvesterName) { TransportManager transportManager = queryTransportManager(); return (transportManager == null) ? null : transportManager.getNbHarvesting(harvesterName); } /** * Returns the peer that is this media handler's "raison d'etre". * * @return the {@link MediaAwareCallPeer} that this handler is servicing. */ public T getPeer() { return peer; } /** * Gets the last-known SSRC of an RTP stream with a specific * MediaType received by a MediaStream of this instance. * * @return the last-known SSRC of an RTP stream with a specific * MediaType received by a MediaStream of this instance */ public long getRemoteSSRC(MediaType mediaType) { return mediaHandler.getRemoteSSRC(this, mediaType); } /** * Returns the {@link DynamicRTPExtensionsRegistry} instance we are * currently using. * * @return the {@link DynamicRTPExtensionsRegistry} instance we are * currently using. */ protected DynamicRTPExtensionsRegistry getRtpExtensionsRegistry() { return this.rtpExtensionsRegistry; } /** * Gets the SrtpControls of the MediaStreams of this * instance. * * @return the SrtpControls of the MediaStreams of this * instance */ public SrtpControls getSrtpControls() { return mediaHandler.getSrtpControls(this); } /** * Gets the MediaStream of this CallPeerMediaHandler which * is of a specific MediaType. If this instance doesn't have such a * MediaStream, returns null * * @param mediaType the MediaType of the MediaStream to * retrieve * @return the MediaStream of this CallPeerMediaHandler * which is of the specified mediaType if this instance has such a * MediaStream; otherwise, null */ public MediaStream getStream(MediaType mediaType) { switch (mediaType) { case AUDIO: return audioStream; case DATA: /* * DATA is a valid MediaType value and CallPeerMediaHandler does not * utilize it at this time so no IllegalArgumentException is thrown * and null is returned (as documented). */ return null; case VIDEO: return videoStream; default: throw new IllegalArgumentException("mediaType"); } } /** * Returns the total harvesting time (in ms) for all harvesters. * * @return The total harvesting time (in ms) for all the harvesters. 0 if * the ICE agent is null, or if the agent has nevers harvested. */ public long getTotalHarvestingTime() { TransportManager transportManager = queryTransportManager(); return (transportManager == null) ? null : transportManager.getTotalHarvestingTime(); } /** * Gets the TransportManager implementation handling our address * management. If the TransportManager does not exist yet, it is * created. * * @return the TransportManager implementation handling our address * management */ protected abstract TransportManager getTransportManager(); /** * Gets the TransportManager implementation handling our address * management. If the TransportManager does not exist yet, it is * not created. * * @return the TransportManager implementation handling our address * management */ protected abstract TransportManager queryTransportManager(); /** * Gets the visual Component in which video from the remote peer is * currently being rendered or null if there is currently no video * streaming from the remote peer. * * @return the visual Component in which video from the remote peer * is currently being rendered or null if there is currently no * video streaming from the remote peer */ @Deprecated public Component getVisualComponent() { List visualComponents = getVisualComponents(); return visualComponents.isEmpty() ? null : visualComponents.get(0); } /** * Gets the visual Components in which videos from the remote peer * are currently being rendered. * * @return the visual Components in which videos from the remote * peer are currently being rendered */ public List getVisualComponents() { MediaStream videoStream = getStream(MediaType.VIDEO); List visualComponents; if (videoStream == null) visualComponents = Collections.emptyList(); else { visualComponents = ((VideoMediaStream) videoStream).getVisualComponents(); } return visualComponents; } /** * Creates if necessary, and configures the stream that this * MediaHandler is using for the MediaType matching the * one of the MediaDevice. * * @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(StreamConnector connector, MediaDevice device, MediaFormat format, MediaStreamTarget target, MediaDirection direction, List rtpExtensions, boolean masterStream) throws OperationFailedException { MediaType mediaType = device.getMediaType(); if (logger.isDebugEnabled()) logger.debug("Initializing " + mediaType + " stream for " +getPeer()); /* * Do make sure that no unintentional streaming of media generated by * the user without prior consent will happen. */ direction = direction.and(getDirectionUserPreference(mediaType)); /* * If the device does not support a direction, there is really nothing * to be done at this point to make it use it. */ direction = direction.and(device.getDirection()); MediaStream stream = mediaHandler.initStream( this, connector, device, format, target, direction, rtpExtensions, masterStream); switch (mediaType) { case AUDIO: audioStream = (AudioMediaStream) stream; break; case VIDEO: videoStream = (VideoMediaStream) stream; break; } return stream; } /** * Compares a list of MediaFormats offered by a remote party * to the list of locally supported RTPExtensions as returned * by one of our local MediaDevices and returns a third * List that contains their intersection. * * Note that it also treats telephone-event as a special case and puts it * to the end of the intersection, if there is any intersection. * * @param remoteFormats remote MediaFormat found in the * SDP message * @param localFormats local supported MediaFormat of our device * @return intersection between our local and remote MediaFormat */ protected List intersectFormats( List remoteFormats, List localFormats) { List ret = new ArrayList(); MediaFormat telephoneEvents = null; MediaFormat red = null; MediaFormat ulpfec = null; for(MediaFormat remoteFormat : remoteFormats) { MediaFormat localFormat = findMediaFormat(localFormats, remoteFormat); if (localFormat != null) { // We ignore telephone-event, red and ulpfec here as they are // not real media formats. Therefore we don't want to decide to // use any of them as our preferred format. We'll add them back // later if we find a common media format. // // Note if there are multiple telephone-event (or red, or // ulpfec) formats, we'll lose all but the last one. That's // fine because it's meaningless to have multiple repeated // formats. String encoding = localFormat.getEncoding(); if (Constants.TELEPHONE_EVENT.equals(encoding)) { telephoneEvents = localFormat; continue; } else if (Constants.RED.equals(encoding)) { red = localFormat; continue; } else if (Constants.ULPFEC.equals(encoding)) { ulpfec = localFormat; continue; } ret.add(localFormat); } } // If we've found some compatible formats, add telephone-event, red // and ulpfec back in to the end of the list (if we removed any of them) // above. If we didn't find any compatible formats, we don't want to // add any of these formats as the only entries in the list because // there'd be no media. if (!ret.isEmpty()) { if (telephoneEvents != null) ret.add(telephoneEvents); if (red != null) ret.add(red); if (ulpfec != null) ret.add(ulpfec); } return ret; } /** * Compares a list of RTPExtensions offered by a remote party * to the list of locally supported RTPExtensions as returned * by one of our local MediaDevices and returns a third * List that contains their intersection. The returned * List contains extensions supported by both the remote party and * the local device that we are dealing with. Direction attributes of both * lists are also intersected and the returned RTPExtensions have * directions valid from a local perspective. In other words, if * remoteExtensions contains an extension that the remote party * supports in a SENDONLY mode, and we support that extension in a * SENDRECV mode, the corresponding entry in the returned list will * have a RECVONLY direction. * * @param remoteExtensions the List of RTPExtensions as * advertised by the remote party. * @param supportedExtensions the List of RTPExtensions * that a local MediaDevice returned as supported. * * @return the (possibly empty) intersection of both of the extensions lists * in a form that can be used for generating an SDP media description or * for configuring a stream. */ protected List intersectRTPExtensions( List remoteExtensions, List supportedExtensions) { if(remoteExtensions == null || supportedExtensions == null) return new ArrayList(); List intersection = new ArrayList( Math.min(remoteExtensions.size(), supportedExtensions.size())); //loop through the list that the remote party sent for(RTPExtension remoteExtension : remoteExtensions) { RTPExtension localExtension = findExtension( supportedExtensions, remoteExtension.getURI().toString()); if(localExtension == null) continue; MediaDirection localDir = localExtension.getDirection(); MediaDirection remoteDir = remoteExtension.getDirection(); RTPExtension intersected = new RTPExtension( localExtension.getURI(), localDir.getDirectionForAnswer(remoteDir), remoteExtension.getExtensionAttributes()); intersection.add(intersected); } return intersection; } /** * Checks whether dev can be used for a call. * * @return true if the device is not null, and it has at least * one enabled format. Otherwise false */ public boolean isDeviceActive(MediaDevice dev) { return (dev != null) && !getLocallySupportedFormats(dev).isEmpty(); } /** * Checks whether dev can be used for a call, using * sendPreset and reveicePreset * * @return true if the device is not null, and it has at least * one enabled format. Otherwise false */ public boolean isDeviceActive( MediaDevice dev, QualityPreset sendPreset, QualityPreset receivePreset) { return (dev != null) && !getLocallySupportedFormats(dev, sendPreset, receivePreset) .isEmpty(); } /** * Determines whether this media handler is currently set to transmit local * audio. * * @return true if the media handler is set to transmit local audio * and false otherwise. */ public boolean isLocalAudioTransmissionEnabled() { return audioDirectionUserPreference.allowsSending(); } /** * Determines whether this handler's streams have been placed on hold. * * @return true if this handler's streams have been placed on hold * and false otherwise. */ public boolean isLocallyOnHold() { return locallyOnHold; //no need to actually check stream directions because we only update //them through the setLocallyOnHold() method so if the value of the //locallyOnHold field has changed, so have stream directions. } /** * Determines whether this media handler is currently set to transmit local * video. * * @return true if the media handler is set to transmit local video * and false otherwise. */ public boolean isLocalVideoTransmissionEnabled() { return videoDirectionUserPreference.allowsSending(); } /** * Determines whether the audio stream of this media handler is currently * on mute. * * @return true if local audio transmission is currently on mute * and false otherwise. */ public boolean isMute() { MediaStream audioStream = getStream(MediaType.AUDIO); return (audioStream != null) && audioStream.isMute(); } /** * Determines whether the remote party has placed all our streams on hold. * * @return true if all our streams have been placed on hold (i.e. * if none of them is currently sending and false otherwise. */ public boolean isRemotelyOnHold() { for (MediaType mediaType : MediaType.values()) { MediaStream stream = getStream(mediaType); if ((stream != null) && stream.getDirection().allowsSending()) return false; } return true; } /** * Determines whether RTP translation is enabled for the CallPeer * represented by this CallPeerMediaHandler and for a specific * MediaType. * * @param mediaType the MediaType for which it is to be determined * whether RTP translation is enabled for the CallPeeer represented * by this CallPeerMediaHandler * @return true if RTP translation is enabled for the * CallPeer represented by this CallPeerMediaHandler and * for the specified mediaType; otherwise, false */ public boolean isRTPTranslationEnabled(MediaType mediaType) { T peer = getPeer(); MediaAwareCall call = peer.getCall(); if ((call != null) && call.isConferenceFocus() && !call.isLocalVideoStreaming()) { Iterator callPeerIt = call.getCallPeers(); while (callPeerIt.hasNext()) { MediaAwareCallPeer callPeer = (MediaAwareCallPeer) callPeerIt.next(); MediaStream stream = callPeer.getMediaHandler().getStream(mediaType); if (stream != null) return true; } } return false; } /** * Returns the secure state of the call. If both audio and video is secured. * * @return the call secure state */ public boolean isSecure() { for (MediaType mediaType : MediaType.values()) { MediaStream stream = getStream(mediaType); /* * If a stream for a specific MediaType does not exist, it's * considered secure. */ if ((stream != null) && !stream.getSrtpControl().getSecureCommunicationStatus()) return false; } return true; } /** * Notifies this instance about a PropertyChangeEvent fired by the * associated {@link MediaHandler}. Since this instance wraps around the * associated MediaHandler, it forwards the property changes as its * own. Allows extenders to override. * * @param ev the PropertyChangeEvent fired by the associated * MediaHandler */ protected void mediaHandlerPropertyChange(PropertyChangeEvent ev) { firePropertyChange( ev.getPropertyName(), ev.getOldValue(), ev.getNewValue()); } /** * Processes a request for a (video) key frame from the remote peer to the * local peer. * * @return true if the request for a (video) key frame has been * honored by the local peer; otherwise, false */ public boolean processKeyFrameRequest() { return mediaHandler.processKeyFrameRequest(this); } /** * Removes from this instance and cleans up the SrtpControl which * are not of a specific SrtpControlType. * * @param mediaType the MediaType of the SrtpControl to be * examined * @param srtpControlType the SrtpControlType of the * SrtpControls to not be removed from this instance and cleaned * up. If null, all SrtpControls are removed from this * instance and cleaned up */ protected void removeAndCleanupOtherSrtpControls( MediaType mediaType, SrtpControlType srtpControlType) { SrtpControls srtpControls = getSrtpControls(); for (SrtpControlType i : SrtpControlType.values()) { if (!i.equals(srtpControlType)) { SrtpControl e = srtpControls.remove(mediaType, i); if (e != null) e.cleanup(null); } } } /** * Unregisters a specific VideoListener from this instance so that * it stops receiving notifications from it about changes in the * availability of visual Components displaying video. * * @param listener the VideoListener to be unregistered from this * instance and to stop receiving notifications from it about changes in the * availability of visual Components displaying video */ public void removeVideoListener(VideoListener listener) { videoNotifierSupport.removeVideoListener(listener); } /** * Requests a key frame from the remote peer of the associated * VideoMediaStream of this CallPeerMediaHandler. The * default implementation provided by CallPeerMediaHandler always * returns false. * * @return true if this CallPeerMediaHandler has indeed * requested a key frame from the remote peer of its associated * VideoMediaStream in response to the call; otherwise, * false */ protected boolean requestKeyFrame() { return false; } /** * Sends empty UDP packets to target destination data/control ports in order * to open port on NAT or RTP proxy if any. In order to be really efficient, * this method should be called after we send our offer or answer. * * @param target MediaStreamTarget */ protected void sendHolePunchPacket(MediaStreamTarget target) { getTransportManager().sendHolePunchPacket(target, MediaType.VIDEO); } /** * Sets csrcAudioLevelListener as the listener that will be * receiving notifications for changes in the audio levels of the remote * participants that our peer is mixing. * * @param listener the CsrcAudioLevelListener to set to our audio * stream. */ public void setCsrcAudioLevelListener(CsrcAudioLevelListener listener) { synchronized (csrcAudioLevelListenerLock) { if (this.csrcAudioLevelListener != listener) { MediaHandler mediaHandler = getMediaHandler(); if ((mediaHandler != null) && (this.csrcAudioLevelListener != null)) { mediaHandler.removeCsrcAudioLevelListener( this.csrcAudioLevelListener); } this.csrcAudioLevelListener = listener; if ((mediaHandler != null) && (this.csrcAudioLevelListener != null)) { mediaHandler.addCsrcAudioLevelListener( this.csrcAudioLevelListener); } } } } /** * Specifies whether this media handler should be allowed to transmit * local audio. * * @param enabled true if the media handler should transmit local * audio and false otherwise. */ public void setLocalAudioTransmissionEnabled(boolean enabled) { audioDirectionUserPreference = enabled ? MediaDirection.SENDRECV : MediaDirection.RECVONLY; } /** * Puts all MediaStreams in this handler locally on or off hold * (according to the value of locallyOnHold). This would also be * taken into account when the next update offer is generated. * * @param locallyOnHold true if we are to make our streams * stop transmitting and false if we are to start transmitting * again. */ public void setLocallyOnHold(boolean locallyOnHold) { if (logger.isDebugEnabled()) logger.debug("Setting locally on hold: " + locallyOnHold); this.locallyOnHold = locallyOnHold; // On hold. if(locallyOnHold) { MediaStream audioStream = getStream(MediaType.AUDIO); MediaDirection direction = (getPeer().getCall().isConferenceFocus() || audioStream == null) ? MediaDirection.INACTIVE : audioStream.getDirection().and(MediaDirection.SENDONLY); // the direction in situation where audioStream is // null is ignored (just avoiding NPE) if(audioStream != null) { audioStream.setDirection(direction); audioStream.setMute(true); } MediaStream videoStream = getStream(MediaType.VIDEO); if(videoStream != null) { direction = getPeer().getCall().isConferenceFocus() ? MediaDirection.INACTIVE : videoStream.getDirection().and(MediaDirection.SENDONLY); /* * Set the video direction to INACTIVE, because currently we * cannot mute video streams. */ videoStream.setDirection(MediaDirection.INACTIVE); //videoStream.setDirection(direction); //videoStream.setMute(true); } } /* * Off hold. Make sure that we re-enable sending only if other party is * not on hold. */ else if (!CallPeerState.ON_HOLD_MUTUALLY.equals(getPeer().getState())) { MediaStream audioStream = getStream(MediaType.AUDIO); if(audioStream != null) { audioStream.setDirection( audioStream.getDirection().or(MediaDirection.SENDONLY)); audioStream.setMute(false); } MediaStream videoStream = getStream(MediaType.VIDEO); if((videoStream != null) && (videoStream.getDirection() != MediaDirection.INACTIVE)) { videoStream.setDirection( videoStream.getDirection().or(MediaDirection.SENDONLY)); videoStream.setMute(false); } } } /** * If the local AudioMediaStream has already been created, sets * listener as the SimpleAudioLevelListener that it should * notify for local user level events. Otherwise stores a reference to * listener so that we could add it once we create the stream. * * @param listener the SimpleAudioLevelListener to add or * null if we are trying to remove it. */ public void setLocalUserAudioLevelListener( SimpleAudioLevelListener listener) { synchronized (localUserAudioLevelListenerLock) { if (this.localUserAudioLevelListener != listener) { MediaHandler mediaHandler = getMediaHandler(); if ((mediaHandler != null) && (this.localUserAudioLevelListener != null)) { mediaHandler.removeLocalUserAudioLevelListener( this.localUserAudioLevelListener); } this.localUserAudioLevelListener = listener; if ((mediaHandler != null) && (this.localUserAudioLevelListener != null)) { mediaHandler.addLocalUserAudioLevelListener( this.localUserAudioLevelListener); } } } } /** * Specifies whether this media handler should be allowed to transmit * local video. * * @param enabled true if the media handler should transmit local * video and false otherwise. */ public void setLocalVideoTransmissionEnabled(boolean enabled) { if (logger.isDebugEnabled()) logger.debug("Setting local video transmission enabled: " + enabled); MediaDirection oldValue = videoDirectionUserPreference; videoDirectionUserPreference = enabled ? MediaDirection.SENDRECV : MediaDirection.RECVONLY; MediaDirection newValue = videoDirectionUserPreference; /* * Do not send an event here if the local video is enabled because the * video stream needs to start before the correct MediaDevice is set in * VideoMediaDeviceSession. */ if (!enabled) { firePropertyChange( OperationSetVideoTelephony.LOCAL_VIDEO_STREAMING, oldValue, newValue); } } public void setMediaHandler(MediaHandler mediaHandler) { if (this.mediaHandler != mediaHandler) { if (this.mediaHandler != null) { synchronized (csrcAudioLevelListenerLock) { if (csrcAudioLevelListener != null) { this.mediaHandler.removeCsrcAudioLevelListener( csrcAudioLevelListener); } } synchronized (localUserAudioLevelListenerLock) { if (localUserAudioLevelListener != null) { this.mediaHandler.removeLocalUserAudioLevelListener( localUserAudioLevelListener); } } synchronized (streamAudioLevelListenerLock) { if (streamAudioLevelListener != null) { this.mediaHandler.removeStreamAudioLevelListener( streamAudioLevelListener); } } this.mediaHandler.removeKeyFrameRequester(keyFrameRequester); this.mediaHandler.removePropertyChangeListener( mediaHandlerPropertyChangeListener); if (srtpListener != null) this.mediaHandler.removeSrtpListener(srtpListener); this.mediaHandler.removeVideoListener(videoStreamVideoListener); // We intentionally do not remove our Call from the list of // DTMF listeners. It should stay there as long as the // MediaHandler is used by at least one CallPeer/CPMH. //this.mediaHandler.removeDtmfListener(getPeer().getCall()); } this.mediaHandler = mediaHandler; if (this.mediaHandler != null) { synchronized (csrcAudioLevelListenerLock) { if (csrcAudioLevelListener != null) { this.mediaHandler.addCsrcAudioLevelListener( csrcAudioLevelListener); } } synchronized (localUserAudioLevelListenerLock) { if (localUserAudioLevelListener != null) { this.mediaHandler.addLocalUserAudioLevelListener( localUserAudioLevelListener); } } synchronized (streamAudioLevelListenerLock) { if (streamAudioLevelListener != null) { this.mediaHandler.addStreamAudioLevelListener( streamAudioLevelListener); } } this.mediaHandler.addKeyFrameRequester(-1, keyFrameRequester); this.mediaHandler.addPropertyChangeListener( mediaHandlerPropertyChangeListener); if (srtpListener != null) this.mediaHandler.addSrtpListener(srtpListener); this.mediaHandler.addVideoListener(videoStreamVideoListener); this.mediaHandler.addDtmfListener(getPeer().getCall()); } } } /** * Causes this handler's AudioMediaStream to stop transmitting the * audio being fed from this stream's MediaDevice and transmit * silence instead. * * @param mute true if we are to make our audio stream start * transmitting silence and false if we are to end the transmission * of silence and use our stream's MediaDevice again. */ public void setMute(boolean mute) { MediaStream audioStream = getStream(MediaType.AUDIO); if (audioStream != null) audioStream.setMute(mute); } /** * If the local AudioMediaStream has already been created, sets * listener as the SimpleAudioLevelListener that it should * notify for stream user level events. Otherwise stores a reference to * listener so that we could add it once we create the stream. * * @param listener the SimpleAudioLevelListener to add or * null if we are trying to remove it. */ public void setStreamAudioLevelListener(SimpleAudioLevelListener listener) { synchronized (streamAudioLevelListenerLock) { if (this.streamAudioLevelListener != listener) { MediaHandler mediaHandler = getMediaHandler(); if ((mediaHandler != null) && (this.streamAudioLevelListener != null)) { mediaHandler.removeStreamAudioLevelListener( this.streamAudioLevelListener); } this.streamAudioLevelListener = listener; if ((mediaHandler != null) && (this.streamAudioLevelListener != null)) { mediaHandler.addStreamAudioLevelListener( this.streamAudioLevelListener); } } } } /** * Starts this CallPeerMediaHandler. If it has already been * started, does nothing. * * @throws IllegalStateException if this method is called without this * handler having first seen a media description or having generated an * offer. */ public void start() throws IllegalStateException { if (logger.isInfoEnabled()) logger.info("Starting"); MediaStream stream; stream = getStream(MediaType.AUDIO); if ((stream != null) && !stream.isStarted() && isLocalAudioTransmissionEnabled()) { getTransportManager().setTrafficClass( stream.getTarget(), MediaType.AUDIO); stream.start(); } stream = getStream(MediaType.VIDEO); if (stream != null) { /* * Inform listener of LOCAL_VIDEO_STREAMING only once the video * starts so that VideoMediaDeviceSession has correct MediaDevice * set (switch from desktop streaming to webcam video or vice-versa * issue) */ firePropertyChange( OperationSetVideoTelephony.LOCAL_VIDEO_STREAMING, null, videoDirectionUserPreference); if(!stream.isStarted()) { getTransportManager().setTrafficClass( stream.getTarget(), MediaType.VIDEO); stream.start(); /* * Send an empty packet to unblock some kinds of RTP proxies. Do * not consult whether the local video should be streamed and * send the hole-punch packet anyway to let the remote video * reach this local peer. */ sendHolePunchPacket(stream.getTarget()); } } } /** * Passes multiStreamData to the video stream that we are using * in this media handler (if any) so that the underlying SRTP lib could * properly handle stream security. * * @param master the data that we are supposed to pass to our * video stream. */ public void startSrtpMultistream(SrtpControl master) { MediaStream videoStream = getStream(MediaType.VIDEO); if (videoStream != null) videoStream.getSrtpControl().setMultistream(master); } /** * 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 abstract void throwOperationFailedException( String message, int errorCode, Throwable cause) throws OperationFailedException; /** * Returns the value to use for the 'msid' source-specific SDP media * attribute (RFC5576) for the stream of type mediaType towards * the CallPeer. It consist of a group identifier (shared between * the local audio and video streams towards the CallPeer) and an * identifier for the particular stream, separated by a space. * * {@see http://tools.ietf.org/html/draft-ietf-mmusic-msid} * * @param mediaType the media type of the stream for which to return the * value for 'msid' * @return the value to use for the 'msid' source-specific SDP media * attribute (RFC5576) for the stream of type mediaType towards * the CallPeer. */ public String getMsid(MediaType mediaType) { return msLabel + " " + getLabel(mediaType); } /** * Returns the value to use for the 'label' source-specific SDP media * attribute (RFC5576) for the stream of type mediaType towards * the CallPeer. * * @param mediaType the media type of the stream for which to return the * value for 'label' * @return the value to use for the 'label' source-specific SDP media * attribute (RFC5576) for the stream of type mediaType towards * the CallPeer. */ public String getLabel(MediaType mediaType) { return mediaType.toString() + hashCode(); } /** * Returns the value to use for the 'mslabel' source-specific SDP media * attribute (RFC5576). * @return the value to use for the 'mslabel' source-specific SDP media * attribute (RFC5576). */ public String getMsLabel() { return msLabel; } /** * Represents the PropertyChangeListener which listens to changes * in the values of the properties of the Call of {@link #peer}. * Remembers the Call it has been added to because peer * does not have a call anymore at the time {@link #close()} is * called. */ private class CallPropertyChangeListener implements PropertyChangeListener { /** * The Call this PropertyChangeListener will be or is * already added to. */ private final MediaAwareCall call; /** * Initializes a new CallPropertyChangeListener which is to be * added to a specific Call. * * @param call the Call the new instance is to be added to */ public CallPropertyChangeListener(MediaAwareCall call) { this.call = call; } /** * Notifies this instance that the value of a specific property of * {@link #call} has changed from a specific old value to a specific * new value. * * @param event a PropertyChangeEvent which specifies the name * of the property which had its value changed and the old and new * values */ public void propertyChange(PropertyChangeEvent event) { callPropertyChange(event); } /** * Removes this PropertyChangeListener from its associated * Call. */ public void removePropertyChangeListener() { call.removePropertyChangeListener(this); } } }