/*
* 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 <T> the peer extension class like for example <tt>CallPeerSipImpl</tt>
* or <tt>CallPeerJabberImpl</tt>
*
* @author Emil Ivov
* @author Lyubomir Marinov
* @author Boris Grozev
*/
public abstract class CallPeerMediaHandler<T extends MediaAwareCallPeer<?,?,?>>
extends PropertyChangeNotifier
{
/**
* The <tt>Logger</tt> used by the <tt>CallPeerMediaHandler</tt> class and
* its instances for logging output.
*/
private static final Logger logger
= Logger.getLogger(CallPeerMediaHandler.class);
/**
* The name of the <tt>CallPeerMediaHandler</tt> property which specifies
* the local SSRC of its audio <tt>MediaStream</tt>.
*/
public static final String AUDIO_LOCAL_SSRC = "AUDIO_LOCAL_SSRC";
/**
* The name of the <tt>CallPeerMediaHandler</tt> property which specifies
* the remote SSRC of its audio <tt>MediaStream</tt>.
*/
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 <tt>CallPeerMediaHandler</tt> property which specifies
* the local SSRC of its video <tt>MediaStream</tt>.
*/
public static final String VIDEO_LOCAL_SSRC = "VIDEO_LOCAL_SSRC";
/**
* The name of the <tt>CallPeerMediaHandler</tt> property which specifies
* the remote SSRC of its video <tt>MediaStream</tt>.
*/
public static final String VIDEO_REMOTE_SSRC = "VIDEO_REMOTE_SSRC";
/**
* The initial content of a hole punch packet. It has some fields pre-set.
* Like rtp verion, sequence number and timestamp.
*/
private static final byte[] HOLE_PUNCH_PACKET =
{
(byte)0x80, 0x00, 0x02, (byte)0x9E, 0x00, 0x09,
(byte)0xD0, (byte)0x80, 0x00, 0x00, 0x00, (byte)0x00,
};
/**
* List of advertised encryption methods. Indicated before establishing the
* call.
*/
private List<SrtpControlType> advertisedEncryptionMethods =
new ArrayList<SrtpControlType>();
/**
* Determines whether or not streaming local audio is currently enabled.
*/
private MediaDirection audioDirectionUserPreference
= MediaDirection.SENDRECV;
/**
* The <tt>AudioMediaStream</tt> which this instance uses to send and
* receive audio.
*/
private AudioMediaStream audioStream;
/**
* The <tt>PropertyChangeListener</tt> which listens to changes in the
* values of the properties of the <tt>Call</tt> of {@link #peer}.
*/
private final CallPropertyChangeListener callPropertyChangeListener;
/**
* The listener that our <tt>CallPeer</tt> registers for CSRC audio level
* events.
*/
private CsrcAudioLevelListener csrcAudioLevelListener;
/**
* The object that we are using to sync operations on
* <tt>csrcAudioLevelListener</tt>.
*/
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 <tt>KeyFrameRequester</tt> implemented by this
* <tt>CallPeerMediaHandler</tt>.
*/
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 <tt>CallPeer</tt> registered for local user audio
* level events.
*/
private SimpleAudioLevelListener localUserAudioLevelListener;
/**
* The object that we are using to sync operations on
* <tt>localAudioLevelListener</tt>.
*/
private final Object localUserAudioLevelListenerLock = new Object();
/**
* The state of this instance which may be shared with multiple other
* <tt>CallPeerMediaHandler</tt>s.
*/
private MediaHandler mediaHandler;
/**
* The <tt>PropertyChangeListener</tt> which listens to changes in the
* values of the properties of the <tt>MediaStream</tt>s of this instance.
* Since <tt>CallPeerMediaHandler</tt> wraps around/shares a
* <tt>MediaHandler</tt>, <tt>mediaHandlerPropertyChangeListener</tt>
* actually listens to <tt>PropertyChangeEvent</tt>s fired by the
* <tt>MediaHandler</tt> in question and forwards them as its own.
*/
private final PropertyChangeListener mediaHandlerPropertyChangeListener
= new PropertyChangeListener()
{
/**
* Notifies this <tt>PropertyChangeListener</tt> that the value of
* a specific property of the notifier it is registered with has
* changed.
*
* @param ev a <tt>PropertyChangeEvent</tt> 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 <tt>SrtpListener</tt> which is responsible for the SRTP control. Most
* often than not, it is the <tt>peer</tt> itself.
*/
private final SrtpListener srtpListener;
/**
* The listener that our <tt>CallPeer</tt> registered for stream audio
* level events.
*/
private SimpleAudioLevelListener streamAudioLevelListener;
/**
* The object that we are using to sync operations on
* <tt>streamAudioLevelListener</tt>.
*/
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
* <tt>VideoListener</tt>s and firing <tt>VideoEvent</tt>s to them on behalf
* of this instance.
*/
private final VideoNotifierSupport videoNotifierSupport
= new VideoNotifierSupport(this, true);
/**
* The <tt>VideoMediaStream</tt> which this instance uses to send and
* receive video.
*/
private VideoMediaStream videoStream;
/**
* Identifier used to group the audio stream and video stream towards
* the <tt>CallPeer</tt> in SDP.
*/
private String msLabel = UUID.randomUUID().toString();
/**
* The <tt>VideoListener</tt> which listens to the video
* <tt>MediaStream</tt> of this instance for changes in the availability of
* visual <tt>Component</tt>s displaying remote video and re-fires them as
* originating from this instance.
*/
private final VideoListener videoStreamVideoListener
= new VideoListener()
{
/**
* Notifies this <tt>VideoListener</tt> about a specific
* <tt>VideoEvent</tt>. Fires a new <tt>VideoEvent</tt> which has
* this <tt>CallPeerMediaHandler</tt> as its source and carries the
* same information as the specified <tt>ev</tt> i.e. translates the
* specified <tt>ev</tt> into a <tt>VideoEvent</tt> fired by this
* <tt>CallPeerMediaHandler</tt>.
*
* @param ev the <tt>VideoEvent</tt> to notify this
* <tt>VideoListener</tt> 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
* <tt>peer</tt>.
*
* @param peer the <tt>CallPeer</tt> 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 <tt>VideoListener</tt> with this instance so that it
* starts receiving notifications from it about changes in the availability
* of visual <tt>Component</tt>s displaying video.
*
* @param listener the <tt>VideoListener</tt> to be registered with this
* instance and to start receiving notifications from it about changes in
* the availability of visual <tt>Component</tt>s displaying video
*/
public void addVideoListener(VideoListener listener)
{
videoNotifierSupport.addVideoListener(listener);
}
/**
* Notifies this instance that a value of a specific property of the
* <tt>Call</tt> of {@link #peer} has changed from a specific old value to a
* specific new value.
*
* @param ev a <tt>PropertyChangeEvent</tt> 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 : new MediaType[] {MediaType.AUDIO, MediaType.VIDEO} )
{
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 <tt>MediaStream</tt> that this instance uses for a specific
* <tt>MediaType</tt> and prepares it for garbage collection.
*
* @param mediaType the <tt>MediaType</tt> 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 <tt>RTPExtension</tt> in <tt>extList</tt> that uses
* the specified <tt>extensionURN</tt> or <tt>null</tt> if <tt>extList</tt>
* did not contain such an extension.
*
* @param extList the <tt>List</tt> that we will be looking through.
* @param extensionURN the URN of the <tt>RTPExtension</tt> that we are
* looking for.
*
* @return the first <tt>RTPExtension</tt> in <tt>extList</tt> that uses
* the specified <tt>extensionURN</tt> or <tt>null</tt> if <tt>extList</tt>
* did not contain such an extension.
*/
private RTPExtension findExtension(List<RTPExtension> extList,
String extensionURN)
{
for(RTPExtension rtpExt : extList)
if (rtpExt.getURI().toASCIIString().equals(extensionURN))
return rtpExt;
return null;
}
/**
* Finds a <tt>MediaFormat</tt> in a specific list of <tt>MediaFormat</tt>s
* which matches a specific <tt>MediaFormat</tt>.
*
* @param formats the list of <tt>MediaFormat</tt>s to find the specified
* matching <tt>MediaFormat</tt> into
* @param format encoding of the <tt>MediaFormat</tt> to find
* @return the <tt>MediaFormat</tt> from <tt>formats</tt> which matches
* <tt>format</tt> if such a match exists in <tt>formats</tt>; otherwise,
* <tt>null</tt>
*/
protected MediaFormat findMediaFormat(
List<MediaFormat> formats, MediaFormat format)
{
for(MediaFormat match : formats)
{
if (match.matches(format))
return match;
}
return null;
}
/**
* Notifies the <tt>VideoListener</tt>s registered with this
* <tt>CallPeerMediaHandler</tt> about a specific type of change in the
* availability of a specific visual <tt>Component</tt> depicting video.
*
* @param type the type of change as defined by <tt>VideoEvent</tt> in the
* availability of the specified visual <tt>Component</tt> depicting video
* @param visualComponent the visual <tt>Component</tt> depicting video
* which has been added or removed in this <tt>CallPeerMediaHandler</tt>
* @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 <tt>true</tt> if this event and, more specifically, the visual
* <tt>Component</tt> it describes have been consumed and should be
* considered owned, referenced (which is important because
* <tt>Component</tt>s belong to a single <tt>Container</tt> at a time);
* otherwise, <tt>false</tt>
*/
protected boolean fireVideoEvent(
int type,
Component visualComponent,
int origin)
{
return
videoNotifierSupport.fireVideoEvent(
type, visualComponent, origin,
true);
}
/**
* Notifies the <tt>VideoListener</tt>s registered with this
* <tt>CallPeerMediaHandler</tt> about a specific <tt>VideoEvent</tt>.
*
* @param event the <tt>VideoEvent</tt> to fire to the
* <tt>VideoListener</tt>s registered with this
* <tt>CallPeerMediaHandler</tt>
*/
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 <tt>MediaDevice</tt> which is capable of capture and/or playback
* of media of the specified <tt>MediaType</tt>, is the default choice of
* the user for a <tt>MediaDevice</tt> with the specified <tt>MediaType</tt>
* and is appropriate for the current states of the associated
* <tt>CallPeer</tt> and <tt>Call</tt>.
* <p>
* For example, when the local peer is acting as a conference focus in the
* <tt>Call</tt> of the associated <tt>CallPeer</tt>, the audio device must
* be a mixer.
* </p>
*
* @param mediaType the <tt>MediaType</tt> in which the retrieved
* <tt>MediaDevice</tt> is to capture and/or play back media
* @return a <tt>MediaDevice</tt> which is capable of capture and/or
* playback of media of the specified <tt>mediaType</tt>, is the default
* choice of the user for a <tt>MediaDevice</tt> with the specified
* <tt>mediaType</tt> and is appropriate for the current states of the
* associated <tt>CallPeer</tt> and <tt>Call</tt>
*/
protected MediaDevice getDefaultDevice(MediaType mediaType)
{
return getPeer().getCall().getDefaultDevice(mediaType);
}
/**
* Gets the <tt>MediaDirection</tt> value which represents the preference of
* the user with respect to streaming media of the specified
* <tt>MediaType</tt>.
*
* @param mediaType the <tt>MediaType</tt> to retrieve the user preference
* for
* @return a <tt>MediaDirection</tt> value which represents the preference
* of the user with respect to streaming media of the specified
* <tt>mediaType</tt>
*/
protected MediaDirection getDirectionUserPreference(MediaType mediaType)
{
switch (mediaType)
{
case AUDIO:
return audioDirectionUserPreference;
case VIDEO:
return videoDirectionUserPreference;
default:
return MediaDirection.INACTIVE;
}
}
/**
* 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 <tt>MediaType</tt> to get the SRTP control type for
* @return the SRTP control type (MIKEY, SDES, ZRTP) used for the given
* media type or <tt>null</tt> if SRTP is not enabled for the given media
* type
*/
public SrtpControl getEncryptionMethod(MediaType mediaType)
{
return mediaHandler.getEncryptionMethod(this, mediaType);
}
/**
* Returns a (possibly empty) <tt>List</tt> of <tt>RTPExtension</tt>s
* supported by the device that this media handler uses to handle media of
* the specified <tt>type</tt>.
*
* @param type the <tt>MediaType</tt> of the device whose
* <tt>RTPExtension</tt>s we are interested in.
*
* @return a (possibly empty) <tt>List</tt> of <tt>RTPExtension</tt>s
* supported by the device that this media handler uses to handle media of
* the specified <tt>type</tt>.
*/
protected List<RTPExtension> getExtensionsForType(MediaType type)
{
MediaDevice device = getDefaultDevice(type);
return device != null
? device.getSupportedExtensions()
: new ArrayList<RTPExtension>();
}
/**
* 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)
? 0
: 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 <tt>MediaFormat</tt>s for the
* given <tt>MediaDevice</tt>, ordered in descending priority. Takes into
* account the configuration obtained from the <tt>ProtocolProvider</tt>
* 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 <tt>MediaDevice</tt>.
*
* @return a non-null list of locally supported <tt>MediaFormat</tt>s for
* <tt>mediaDevice</tt>, in decreasing order of priority.
*
* @see CallPeerMediaHandler#getLocallySupportedFormats(MediaDevice,
* QualityPreset, QualityPreset)
*/
public List<MediaFormat> getLocallySupportedFormats(MediaDevice mediaDevice)
{
return getLocallySupportedFormats(mediaDevice, null, null);
}
/**
* Returns a list of locally supported <tt>MediaFormat</tt>s for the
* given <tt>MediaDevice</tt>, ordered in descending priority. Takes into
* account the configuration obtained from the <tt>ProtocolProvider</tt>
* 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 <tt>MediaDevice</tt>.
* @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 <tt>MediaFormat</tt>s for
* <tt>mediaDevice</tt>, in decreasing order of priority.
*/
public List<MediaFormat> getLocallySupportedFormats(
MediaDevice mediaDevice,
QualityPreset sendPreset, QualityPreset receivePreset)
{
if(mediaDevice == null)
return Collections.emptyList();
Map<String, String> 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 <tt>Component</tt>, if any, depicting the video streamed
* from the local peer to the remote peer.
*
* @return the visual <tt>Component</tt> depicting the local video if local
* video is actually being streamed from the local peer to the remote peer;
* otherwise, <tt>null</tt>
*/
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. 0 if this harvester
* does not exists, if the ICE agent is null, or
* if the agent has never harvested with this harvester.
*/
public int getNbHarvesting()
{
TransportManager<?> transportManager = queryTransportManager();
return
(transportManager == null)
? 0
: 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. 0 if this harvester does not exists, if the ICE agent is null, or if the
* agent has never harvested with this harvester.
*/
public int getNbHarvesting(String harvesterName)
{
TransportManager<?> transportManager = queryTransportManager();
return
(transportManager == null)
? 0
: 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
* <tt>MediaType</tt> received by a <tt>MediaStream</tt> of this instance.
*
* @return the last-known SSRC of an RTP stream with a specific
* <tt>MediaType</tt> received by a <tt>MediaStream</tt> 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 <tt>SrtpControl</tt>s of the <tt>MediaStream</tt>s of this
* instance.
*
* @return the <tt>SrtpControl</tt>s of the <tt>MediaStream</tt>s of this
* instance
*/
public SrtpControls getSrtpControls()
{
return mediaHandler.getSrtpControls(this);
}
/**
* Gets the <tt>MediaStream</tt> of this <tt>CallPeerMediaHandler</tt> which
* is of a specific <tt>MediaType</tt>. If this instance doesn't have such a
* <tt>MediaStream</tt>, returns <tt>null</tt>
*
* @param mediaType the <tt>MediaType</tt> of the <tt>MediaStream</tt> to
* retrieve
* @return the <tt>MediaStream</tt> of this <tt>CallPeerMediaHandler</tt>
* which is of the specified <tt>mediaType</tt> if this instance has such a
* <tt>MediaStream</tt>; otherwise, <tt>null</tt>
*/
public MediaStream getStream(MediaType mediaType)
{
switch (mediaType)
{
case AUDIO:
return audioStream;
case VIDEO:
return videoStream;
default:
return null;
}
}
/**
* 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 never harvested.
*/
public long getTotalHarvestingTime()
{
TransportManager<?> transportManager = queryTransportManager();
return
(transportManager == null)
? 0
: transportManager.getTotalHarvestingTime();
}
/**
* Gets the <tt>TransportManager</tt> implementation handling our address
* management. If the <tt>TransportManager</tt> does not exist yet, it is
* created.
*
* @return the <tt>TransportManager</tt> implementation handling our address
* management
*/
protected abstract TransportManager<T> getTransportManager();
/**
* Gets the <tt>TransportManager</tt> implementation handling our address
* management. If the <tt>TransportManager</tt> does not exist yet, it is
* not created.
*
* @return the <tt>TransportManager</tt> implementation handling our address
* management
*/
protected abstract TransportManager<T> queryTransportManager();
/**
* Gets the visual <tt>Component</tt> in which video from the remote peer is
* currently being rendered or <tt>null</tt> if there is currently no video
* streaming from the remote peer.
*
* @return the visual <tt>Component</tt> in which video from the remote peer
* is currently being rendered or <tt>null</tt> if there is currently no
* video streaming from the remote peer
*/
@Deprecated
public Component getVisualComponent()
{
List<Component> visualComponents = getVisualComponents();
return visualComponents.isEmpty() ? null : visualComponents.get(0);
}
/**
* Gets the visual <tt>Component</tt>s in which videos from the remote peer
* are currently being rendered.
*
* @return the visual <tt>Component</tt>s in which videos from the remote
* peer are currently being rendered
*/
public List<Component> getVisualComponents()
{
MediaStream videoStream = getStream(MediaType.VIDEO);
List<Component> visualComponents;
if (videoStream == null)
visualComponents = Collections.emptyList();
else
{
visualComponents
= ((VideoMediaStream) videoStream).getVisualComponents();
}
return visualComponents;
}
/**
* Creates if necessary, and configures the stream that this
* <tt>MediaHandler</tt> is using for the <tt>MediaType</tt> matching the
* one of the <tt>MediaDevice</tt>.
*
* @param connector the <tt>MediaConnector</tt> that we'd like to bind the
* newly created stream to.
* @param device the <tt>MediaDevice</tt> that we'd like to attach the newly
* created <tt>MediaStream</tt> to.
* @param format the <tt>MediaFormat</tt> that we'd like the new
* <tt>MediaStream</tt> to be set to transmit in.
* @param target the <tt>MediaStreamTarget</tt> containing the RTP and RTCP
* address:port couples that the new stream would be sending packets to.
* @param direction the <tt>MediaDirection</tt> that we'd like the new
* stream to use (i.e. sendonly, sendrecv, recvonly, or inactive).
* @param rtpExtensions the list of <tt>RTPExtension</tt>s that should be
* enabled for this stream.
* @param masterStream whether the stream to be used as master if secured
*
* @return the newly created <tt>MediaStream</tt>.
*
* @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<RTPExtension> 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 <tt>MediaFormat</tt>s offered by a remote party
* to the list of locally supported <tt>RTPExtension</tt>s as returned
* by one of our local <tt>MediaDevice</tt>s and returns a third
* <tt>List</tt> 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 <tt>MediaFormat</tt> found in the
* SDP message
* @param localFormats local supported <tt>MediaFormat</tt> of our device
* @return intersection between our local and remote <tt>MediaFormat</tt>
*/
protected List<MediaFormat> intersectFormats(
List<MediaFormat> remoteFormats,
List<MediaFormat> localFormats)
{
List<MediaFormat> ret = new ArrayList<MediaFormat>();
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 <tt>RTPExtension</tt>s offered by a remote party
* to the list of locally supported <tt>RTPExtension</tt>s as returned
* by one of our local <tt>MediaDevice</tt>s and returns a third
* <tt>List</tt> that contains their intersection. The returned
* <tt>List</tt> 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 <tt>RTPExtension</tt>s have
* directions valid from a local perspective. In other words, if
* <tt>remoteExtensions</tt> contains an extension that the remote party
* supports in a <tt>SENDONLY</tt> mode, and we support that extension in a
* <tt>SENDRECV</tt> mode, the corresponding entry in the returned list will
* have a <tt>RECVONLY</tt> direction.
*
* @param remoteExtensions the <tt>List</tt> of <tt>RTPExtension</tt>s as
* advertised by the remote party.
* @param supportedExtensions the <tt>List</tt> of <tt>RTPExtension</tt>s
* that a local <tt>MediaDevice</tt> 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<RTPExtension> intersectRTPExtensions(
List<RTPExtension> remoteExtensions,
List<RTPExtension> supportedExtensions)
{
if(remoteExtensions == null || supportedExtensions == null)
return new ArrayList<RTPExtension>();
List<RTPExtension> intersection = new ArrayList<RTPExtension>(
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 <tt>dev</tt> can be used for a call.
*
* @return <tt>true</tt> if the device is not null, and it has at least
* one enabled format. Otherwise <tt>false</tt>
*/
public boolean isDeviceActive(MediaDevice dev)
{
return (dev != null) && !getLocallySupportedFormats(dev).isEmpty();
}
/**
* Checks whether <tt>dev</tt> can be used for a call, using
* <tt>sendPreset</tt> and <tt>reveicePreset</tt>
*
* @return <tt>true</tt> if the device is not null, and it has at least
* one enabled format. Otherwise <tt>false</tt>
*/
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 <tt>true</tt> if the media handler is set to transmit local audio
* and <tt>false</tt> otherwise.
*/
public boolean isLocalAudioTransmissionEnabled()
{
return audioDirectionUserPreference.allowsSending();
}
/**
* Determines whether this handler's streams have been placed on hold.
*
* @return <tt>true</tt> if this handler's streams have been placed on hold
* and <tt>false</tt> 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 <tt>true</tt> 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 <tt>true</tt> if local audio transmission is currently on mute
* and <tt>false</tt> 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 <tt>true</tt> if all our streams have been placed on hold (i.e.
* if none of them is currently sending and <tt>false</tt> 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 <tt>CallPeer</tt>
* represented by this <tt>CallPeerMediaHandler</tt> and for a specific
* <tt>MediaType</tt>.
*
* @param mediaType the <tt>MediaType</tt> for which it is to be determined
* whether RTP translation is enabled for the <tT>CallPeeer</tt> represented
* by this <tt>CallPeerMediaHandler</tt>
* @return <tt>true</tt> if RTP translation is enabled for the
* <tt>CallPeer</tt> represented by this <tt>CallPeerMediaHandler</tt> and
* for the specified <tt>mediaType; otherwise, <tt>false</tt>
*/
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 <tt>PropertyChangeEvent</tt> fired by the
* associated {@link MediaHandler}. Since this instance wraps around the
* associated <tt>MediaHandler</tt>, it forwards the property changes as its
* own. Allows extenders to override.
*
* @param ev the <tt>PropertyChangeEvent</tt> fired by the associated
* <tt>MediaHandler</tt>
*/
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 <tt>true</tt> if the request for a (video) key frame has been
* honored by the local peer; otherwise, <tt>false</tt>
*/
public boolean processKeyFrameRequest()
{
return mediaHandler.processKeyFrameRequest(this);
}
/**
* Removes from this instance and cleans up the <tt>SrtpControl</tt> which
* are not of a specific <tt>SrtpControlType</tt>.
*
* @param mediaType the <tt>MediaType</tt> of the <tt>SrtpControl</tt> to be
* examined
* @param srtpControlType the <tt>SrtpControlType</tt> of the
* <tt>SrtpControl</tt>s to not be removed from this instance and cleaned
* up. If <tt>null</tt>, all <tt>SrtpControl</tt>s 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 <tt>VideoListener</tt> from this instance so that
* it stops receiving notifications from it about changes in the
* availability of visual <tt>Component</tt>s displaying video.
*
* @param listener the <tt>VideoListener</tt> to be unregistered from this
* instance and to stop receiving notifications from it about changes in the
* availability of visual <tt>Component</tt>s displaying video
*/
public void removeVideoListener(VideoListener listener)
{
videoNotifierSupport.removeVideoListener(listener);
}
/**
* Requests a key frame from the remote peer of the associated
* <tt>VideoMediaStream</tt> of this <tt>CallPeerMediaHandler</tt>. The
* default implementation provided by <tt>CallPeerMediaHandler</tt> always
* returns <tt>false</tt>.
*
* @return <tt>true</tt> if this <tt>CallPeerMediaHandler</tt> has indeed
* requested a key frame from the remote peer of its associated
* <tt>VideoMediaStream</tt> in response to the call; otherwise,
* <tt>false</tt>
*/
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 stream <tt>MediaStream</tt> non-null stream
* @param mediaType <tt>MediaType</tt>
*/
protected void sendHolePunchPacket(MediaStream stream, MediaType mediaType)
{
// send as a hole punch packet a constructed rtp packet
// has the correct payload type and ssrc
RawPacket packet = new RawPacket(
HOLE_PUNCH_PACKET, 0, RawPacket.FIXED_HEADER_SIZE);
MediaFormat format = stream.getFormat();
byte payloadType = format.getRTPPayloadType();
// is this a dynamic payload type.
if (payloadType == MediaFormat.RTP_PAYLOAD_TYPE_UNKNOWN)
{
payloadType = dynamicPayloadTypes.getPayloadType(format);
}
packet.setPayloadType(payloadType);
packet.setSSRC((int)stream.getLocalSourceID());
getTransportManager().sendHolePunchPacket(
stream.getTarget(), mediaType, packet);
}
/**
* Sets <tt>csrcAudioLevelListener</tt> 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 <tt>CsrcAudioLevelListener</tt> 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 <tt>true</tt> if the media handler should transmit local
* audio and <tt>false</tt> otherwise.
*/
public void setLocalAudioTransmissionEnabled(boolean enabled)
{
audioDirectionUserPreference
= enabled ? MediaDirection.SENDRECV : MediaDirection.RECVONLY;
}
/**
* Puts all <tt>MediaStream</tt>s in this handler locally on or off hold
* (according to the value of <tt>locallyOnHold</tt>). This would also be
* taken into account when the next update offer is generated.
*
* @param locallyOnHold <tt>true</tt> if we are to make our streams
* stop transmitting and <tt>false</tt> 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 <tt>AudioMediaStream</tt> has already been created, sets
* <tt>listener</tt> as the <tt>SimpleAudioLevelListener</tt> that it should
* notify for local user level events. Otherwise stores a reference to
* <tt>listener</tt> so that we could add it once we create the stream.
*
* @param listener the <tt>SimpleAudioLevelListener</tt> to add or
* <tt>null</tt> 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 <tt>true</tt> if the media handler should transmit local
* video and <tt>false</tt> 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 <tt>AudioMediaStream</tt> to stop transmitting the
* audio being fed from this stream's <tt>MediaDevice</tt> and transmit
* silence instead.
*
* @param mute <tt>true</tt> if we are to make our audio stream start
* transmitting silence and <tt>false</tt> if we are to end the transmission
* of silence and use our stream's <tt>MediaDevice</tt> again.
*/
public void setMute(boolean mute)
{
MediaStream audioStream = getStream(MediaType.AUDIO);
if (audioStream != null)
audioStream.setMute(mute);
}
/**
* If the local <tt>AudioMediaStream</tt> has already been created, sets
* <tt>listener</tt> as the <tt>SimpleAudioLevelListener</tt> that it should
* notify for stream user level events. Otherwise stores a reference to
* <tt>listener</tt> so that we could add it once we create the stream.
*
* @param listener the <tt>SimpleAudioLevelListener</tt> to add or
* <tt>null</tt> 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 <tt>CallPeerMediaHandler</tt>. 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();
sendHolePunchPacket(stream, MediaType.AUDIO);
}
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, MediaType.VIDEO);
}
}
}
/**
* Passes <tt>multiStreamData</tt> 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
* <tt>OperationFailedException</tt>
* @param errorCode the error code to be assigned to the new
* <tt>OperationFailedException</tt>
* @param cause the <tt>Throwable</tt> that has caused the necessity to log
* an error and have a new <tt>OperationFailedException</tt> 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 <tt>mediaType</tt> towards
* the <tt>CallPeer</tt>. It consist of a group identifier (shared between
* the local audio and video streams towards the <tt>CallPeer</tt>) 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 <tt>mediaType</tt> towards
* the <tt>CallPeer</tt>.
*/
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 <tt>mediaType</tt> towards
* the <tt>CallPeer</tt>.
*
* @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 <tt>mediaType</tt> towards
* the <tt>CallPeer</tt>.
*/
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 <tt>PropertyChangeListener</tt> which listens to changes
* in the values of the properties of the <tt>Call</tt> of {@link #peer}.
* Remembers the <tt>Call</tt> it has been added to because <tt>peer</tt>
* does not have a <tt>call</tt> anymore at the time {@link #close()} is
* called.
*/
private class CallPropertyChangeListener
implements PropertyChangeListener
{
/**
* The <tt>Call</tt> this <tt>PropertyChangeListener</tt> will be or is
* already added to.
*/
private final MediaAwareCall<?, ?, ?> call;
/**
* Initializes a new <tt>CallPropertyChangeListener</tt> which is to be
* added to a specific <tt>Call</tt>.
*
* @param call the <tt>Call</tt> 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 <tt>PropertyChangeEvent</tt> 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 <tt>PropertyChangeListener</tt> from its associated
* <tt>Call</tt>.
*/
public void removePropertyChangeListener()
{
call.removePropertyChangeListener(this);
}
}
}