/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Copyright @ 2015 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.java.sip.communicator.service.protocol.media;
import java.beans.*;
import java.util.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.*;
import org.jitsi.service.neomedia.*;
import org.jitsi.service.neomedia.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/could not be accessed by bundles that are simply
* using the telephony functionalities.
*
* @param <T> the peer extension class like for example <tt>CallSipImpl</tt>
* or <tt>CallJabberImpl</tt>
* @param <U> the media handler extension class like for example
* <tt>CallPeerMediaHandlerSipImpl</tt> or
* <tt>CallPeerMediaHandlerJabberImpl</tt>
* @param <V> the provider extension class like for example
* <tt>ProtocolProviderServiceSipImpl</tt> or
* <tt>ProtocolProviderServiceJabberImpl</tt>
*
* @author Emil Ivov
* @author Lyubomir Marinov
* @author Boris Grozev
*/
public abstract class MediaAwareCallPeer
<T extends MediaAwareCall<?, ?, V>,
U extends CallPeerMediaHandler<?>,
V extends ProtocolProviderService>
extends AbstractCallPeer<T, V>
implements SrtpListener,
CallPeerConferenceListener,
CsrcAudioLevelListener,
SimpleAudioLevelListener
{
/**
* The <tt>Logger</tt> used by the <tt>MediaAwareCallPeer</tt> class and its
* instances for logging output.
*/
private static final Logger logger
= Logger.getLogger(MediaAwareCallPeer.class);
/**
* The call this peer belongs to.
*/
private T call;
/**
* The listeners registered for level changes in the audio of participants
* that this peer might be mixing and that we are not directly communicating
* with.
*/
private final List<ConferenceMembersSoundLevelListener>
conferenceMembersSoundLevelListeners
= new ArrayList<ConferenceMembersSoundLevelListener>();
/**
* A byte array containing the image/photo representing the call peer.
*/
private byte[] image;
/**
* The media handler class handles all media management for a single
* <tt>CallPeer</tt>. This includes initializing and configuring streams,
* generating SDP, handling ICE, etc. One instance of <tt>CallPeer</tt>always
* corresponds to exactly one instance of <tt>CallPeerMediaHandler</tt> and
* both classes are only separated for reasons of readability.
*/
private U mediaHandler;
/**
* The <tt>PropertyChangeListener</tt> which listens to
* {@link CallPeerMediaHandler} for changes in the values of its properties.
*/
private PropertyChangeListener mediaHandlerPropertyChangeListener;
/**
* A string uniquely identifying the peer.
*/
private String peerID;
/**
* The protocol provider that this peer belongs to.
*/
private final V protocolProvider;
/**
* The list of <tt>SoundLevelListener</tt>s interested in level changes in
* the audio we are getting from the remote peer.
* <p>
* It is implemented as a copy-on-write storage because the number of
* additions and removals of <tt>SoundLevelListener</tt>s is expected to be
* far smaller than the number of audio level changes. The access to it is
* to be synchronized using {@link #streamSoundLevelListenersSyncRoot}.
* </p>
*/
private List<SoundLevelListener> streamSoundLevelListeners;
/**
* The <tt>Object</tt> to synchronize the access to
* {@link #streamSoundLevelListeners}.
*/
private final Object streamSoundLevelListenersSyncRoot = new Object();
/**
* The <tt>List</tt> of <tt>PropertyChangeListener</tt>s listening to this
* <tt>CallPeer</tt> for changes in the values of its properties related to
* video.
*/
private final List<PropertyChangeListener> videoPropertyChangeListeners
= new LinkedList<PropertyChangeListener>();
/**
* Represents the last Conference Information (RFC4575) document sent to
* this <tt>CallPeer</tt>. This is always a document with state "full", even
* if the last document actually sent was a "partial"
*/
private ConferenceInfoDocument lastConferenceInfoSent = null;
/**
* The time (as obtained by <tt>System.currentTimeMillis()</tt>) at which
* a Conference Information (RFC4575) document was last sent to this
* <tt>CallPeer</tt>.
*/
private long lastConferenceInfoSentTimestamp = -1;
/**
* The last Conference Information (RFC4575) document sent to us by this
* <tt>CallPeer</tt>. This is always a document with state "full", which is
* only gets updated by "partial" or "deleted" documents.
*/
private ConferenceInfoDocument lastConferenceInfoReceived = null;
/**
* Whether a conference-info document has been scheduled to be sent to this
* <tt>CallPeer</tt>
*/
private boolean confInfoScheduled = false;
/**
* Synchronization object for confInfoScheduled
*/
private final Object confInfoScheduledSyncRoot = new Object();
/**
* Creates a new call peer with address <tt>peerAddress</tt>.
*
* @param owningCall the call that contains this call peer.
*/
public MediaAwareCallPeer(T owningCall)
{
this.call = owningCall;
this.protocolProvider = owningCall.getProtocolProvider();
// create the uid
this.peerID
= String.valueOf(System.currentTimeMillis())
+ String.valueOf(hashCode());
// we listen for events when the call will become focus or not
// of a conference so we will add or remove our sound level listeners
super.addCallPeerConferenceListener(this);
}
/**
* Adds a specific <tt>ConferenceMembersSoundLevelListener</tt> to the list
* of listeners interested in and notified about changes in conference
* members sound level.
*
* @param listener the <tt>ConferenceMembersSoundLevelListener</tt> to add
* @throws NullPointerException if <tt>listener</tt> is <tt>null</tt>
*/
public void addConferenceMembersSoundLevelListener(
ConferenceMembersSoundLevelListener listener)
{
/*
* XXX The uses of the method at the time of this writing rely on being
* able to add a null listener so make it a no-op here.
*/
if (listener == null)
return;
synchronized (conferenceMembersSoundLevelListeners)
{
if (conferenceMembersSoundLevelListeners.size() == 0)
{
// if this is the first listener that's being registered with
// us, we also need to register ourselves as a CSRC audio level
// listener with the media handler.
getMediaHandler().setCsrcAudioLevelListener(this);
}
conferenceMembersSoundLevelListeners.add(listener);
}
}
/**
* Adds a specific <tt>SoundLevelListener</tt> to the list of listeners
* interested in and notified about changes in the sound level of the audio
* sent by the remote party. When the first listener is being registered
* the method also registers its single listener with the media handler so
* that it would receive level change events and delegate them to the
* listeners that have registered with us.
*
* @param listener the <tt>SoundLevelListener</tt> to add
*/
public void addStreamSoundLevelListener(SoundLevelListener listener)
{
synchronized (streamSoundLevelListenersSyncRoot)
{
if ((streamSoundLevelListeners == null)
|| streamSoundLevelListeners.isEmpty())
{
CallPeerMediaHandler<?> mediaHandler = getMediaHandler();
if (isJitsiVideobridge())
{
/*
* When the local user/peer has organized a telephony
* conference utilizing the Jitsi Videobridge server-side
* technology, the server will calculate the audio levels
* and not the client.
*/
mediaHandler.setCsrcAudioLevelListener(this);
}
else
{
/*
* If this is the first listener that's being registered
* with us, we also need to register ourselves as an audio
* level listener with the media handler. We do this so that
* audio levels would only be calculated if anyone is
* interested in receiving them.
*/
mediaHandler.setStreamAudioLevelListener(this);
}
}
/*
* Implement streamAudioLevelListeners as a copy-on-write storage so
* that iterators over it can iterate without
* ConcurrentModificationExceptions.
*/
streamSoundLevelListeners
= (streamSoundLevelListeners == null)
? new ArrayList<SoundLevelListener>()
: new ArrayList<SoundLevelListener>(
streamSoundLevelListeners);
streamSoundLevelListeners.add(listener);
}
}
/**
* Adds a specific <tt>PropertyChangeListener</tt> to the list of
* listeners which get notified when the properties (e.g.
* LOCAL_VIDEO_STREAMING) associated with this <tt>CallPeer</tt> change
* their values.
*
* @param listener the <tt>PropertyChangeListener</tt> to be notified
* when the properties associated with the specified <tt>Call</tt> change
* their values
*/
public void addVideoPropertyChangeListener(PropertyChangeListener listener)
{
if (listener == null)
throw new NullPointerException("listener");
synchronized (videoPropertyChangeListeners)
{
/*
* The video is part of the media-related functionality and thus it
* is the responsibility of mediaHandler. So listen to mediaHandler
* for video-related property changes and re-fire them as
* originating from this instance.
*/
if (!videoPropertyChangeListeners.contains(listener)
&& videoPropertyChangeListeners.add(listener)
&& (mediaHandlerPropertyChangeListener == null))
{
mediaHandlerPropertyChangeListener
= new PropertyChangeListener()
{
public void propertyChange(PropertyChangeEvent event)
{
Iterable<PropertyChangeListener> listeners;
synchronized (videoPropertyChangeListeners)
{
listeners
= new LinkedList<PropertyChangeListener>(
videoPropertyChangeListeners);
}
PropertyChangeEvent thisEvent
= new PropertyChangeEvent(
this,
event.getPropertyName(),
event.getOldValue(),
event.getNewValue());
for (PropertyChangeListener listener : listeners)
listener.propertyChange(thisEvent);
}
};
getMediaHandler()
.addPropertyChangeListener(
mediaHandlerPropertyChangeListener);
}
}
}
/**
* Notified by its very majesty the media service about changes in the audio
* level of the stream coming from this peer, the method generates the
* corresponding events and delivers them to the listeners that have
* registered here.
*
* @param newLevel the new audio level of the audio stream received from the
* remote peer
*/
public void audioLevelChanged(int newLevel)
{
/*
* If we're in a conference in which this CallPeer is the focus and
* we're the only member in it besides the focus, we will not receive
* audio levels in the RTP and our media will instead measure the audio
* levels of the received media. In order to make the UI oblivious of
* the difference, we have to translate the event to the appropriate
* type of listener.
*
* We may end up in a conference call with 0 members if the server
* for some reason doesn't support sip conference (our subscribes
* doesn't go to the focus of the conference) and so we must
* pass the sound levels measured on the stream so we can see
* the stream activity of the call.
*/
int conferenceMemberCount = getConferenceMemberCount();
if ((conferenceMemberCount > 0) && (conferenceMemberCount < 3))
{
long audioRemoteSSRC
= getMediaHandler().getRemoteSSRC(MediaType.AUDIO);
if (audioRemoteSSRC != CallPeerMediaHandler.SSRC_UNKNOWN)
{
audioLevelsReceived(new long[] { audioRemoteSSRC, newLevel });
return;
}
}
fireStreamSoundLevelChanged(newLevel);
}
/**
* Implements {@link CsrcAudioLevelListener#audioLevelsReceived(long[])}.
* Delivers the received audio levels to the
* {@link ConferenceMembersSoundLevelListener}s registered with this
* <tt>MediaAwareCallPeer</tt>..
*
* @param audioLevels the levels that we need to dispatch to all registered
* <tt>ConferenceMemberSoundLevelListeners</tt>.
*/
public void audioLevelsReceived(long[] audioLevels)
{
/*
* When the local user/peer has organized a telephony conference
* utilizing the Jitsi Videobridge server-side technology, the server
* will calculate the audio levels and not the client.
*/
if (isJitsiVideobridge())
{
long audioRemoteSSRC
= getMediaHandler().getRemoteSSRC(MediaType.AUDIO);
if (audioRemoteSSRC != CallPeerMediaHandler.SSRC_UNKNOWN)
{
for (int i = 0; i < audioLevels.length; i += 2)
{
if (audioLevels[i] == audioRemoteSSRC)
{
fireStreamSoundLevelChanged((int) audioLevels[i + 1]);
break;
}
}
}
}
if (getConferenceMemberCount() == 0)
return;
Map<ConferenceMember, Integer> levelsMap
= new HashMap<ConferenceMember, Integer>();
for (int i = 0; i < audioLevels.length; i += 2)
{
ConferenceMember mmbr = findConferenceMember(audioLevels[i]);
if (mmbr != null)
levelsMap.put(mmbr, (int) audioLevels[i + 1]);
}
synchronized (conferenceMembersSoundLevelListeners)
{
int conferenceMemberSoundLevelListenerCount
= conferenceMembersSoundLevelListeners.size();
if (conferenceMemberSoundLevelListenerCount > 0)
{
ConferenceMembersSoundLevelEvent ev
= new ConferenceMembersSoundLevelEvent(this, levelsMap);
for (int i = 0;
i < conferenceMemberSoundLevelListenerCount;
i++)
{
conferenceMembersSoundLevelListeners
.get(i)
.soundLevelChanged(ev);
}
}
}
}
/**
* Does nothing.
* @param evt the event.
*/
public void callPeerAdded(CallPeerEvent evt) {}
/**
* Does nothing.
* @param evt the event.
*/
public void callPeerRemoved(CallPeerEvent evt) {}
/**
* Dummy implementation of {@link CallPeerConferenceListener
* #conferenceFocusChanged(CallPeerConferenceEvent)}.
*
* @param evt ignored
*/
public void conferenceFocusChanged(CallPeerConferenceEvent evt)
{
}
/**
* Called when this peer becomes a mixer. The method add removes this class
* as the stream audio level listener for the media coming from this peer
* because the levels it delivers no longer represent the level of a
* particular member. The method also adds this class as a member (CSRC)
* audio level listener.
*
* @param conferenceEvent the event containing information (that we don't
* really use) on the newly add member.
*/
public void conferenceMemberAdded(CallPeerConferenceEvent conferenceEvent)
{
if (getConferenceMemberCount() > 2)
{
/*
* This peer is now a conference focus with more than three
* participants. It means that this peer is mixing and sending us
* audio for at least two separate participants. We therefore need
* to switch from stream to CSRC level listening.
*/
CallPeerMediaHandler<?> mediaHandler = getMediaHandler();
mediaHandler.setStreamAudioLevelListener(null);
mediaHandler.setCsrcAudioLevelListener(this);
}
}
/**
* Dummy implementation of {@link CallPeerConferenceListener
* #conferenceMemberErrorReceived(CallPeerConferenceEvent)}.
*
* @param ev the event
*/
public void conferenceMemberErrorReceived(CallPeerConferenceEvent ev) {};
/**
* Called when this peer stops being a mixer. The method add removes this
* class as the stream audio level listener for the media coming from this
* peer because the levels it delivers no longer represent the level of a
* particular member. The method also adds this class as a member (CSRC)
* audio level listener.
*
* @param conferenceEvent the event containing information (that we don't
* really use) on the freshly removed member.
*/
public void conferenceMemberRemoved(CallPeerConferenceEvent conferenceEvent)
{
if (getConferenceMemberCount() < 3)
{
/*
* This call peer is no longer mixing audio from multiple sources
* since there's only us and her in the call. We therefore need to
* switch from CSRC to stream level listening.
*/
CallPeerMediaHandler<?> mediaHandler = getMediaHandler();
mediaHandler.setStreamAudioLevelListener(this);
mediaHandler.setCsrcAudioLevelListener(null);
}
}
/**
* Invokes {@link SoundLevelListener#soundLevelChanged(Object, int) on
* the <tt>SoundLevelListener</tt>s interested in the changes of the audio
* stream received from the remote peer i.e. in
* {@link #streamSoundLevelListeners}.
*
* @param newLevel the new value of the sound level to notify
* <tt>streamSoundLevelListeners</tt> about
*/
private void fireStreamSoundLevelChanged(int newLevel)
{
List<SoundLevelListener> streamSoundLevelListeners;
synchronized (streamSoundLevelListenersSyncRoot)
{
/*
* Since the streamAudioLevelListeners field of this
* MediaAwareCallPeer is implemented as a copy-on-write storage,
* just get a reference to it and it should be safe to iterate over it
* without ConcurrentModificationExceptions.
*/
streamSoundLevelListeners = this.streamSoundLevelListeners;
}
if (streamSoundLevelListeners != null)
{
/*
* Iterate over streamAudioLevelListeners using an index rather than
* an Iterator in order to try to reduce the number of allocations
* (as the number of audio level changes is expected to be very
* large).
*/
int streamSoundLevelListenerCount
= streamSoundLevelListeners.size();
for(int i = 0; i < streamSoundLevelListenerCount; i++)
{
streamSoundLevelListeners.get(i).soundLevelChanged(
this,
newLevel);
}
}
}
/**
* Returns a reference to the call that this peer belongs to. Calls
* are created by underlying telephony protocol implementations.
*
* @return a reference to the call containing this peer.
*/
@Override
public T getCall()
{
return call;
}
/**
* The method returns an image representation of the call peer if one is
* available.
*
* @return byte[] a byte array containing the image or null if no image is
* available.
*/
public byte[] getImage()
{
return image;
}
/**
* Returns a reference to the <tt>CallPeerMediaHandler</tt> used by this
* peer. The media handler class handles all media management for a single
* <tt>CallPeer</tt>. This includes initializing and configuring streams,
* generating SDP, handling ICE, etc. One instance of <tt>CallPeer</tt>
* always corresponds to exactly one instance of
* <tt>CallPeerMediaHandler</tt> and both classes are only separated for
* reasons of readability.
*
* @return a reference to the <tt>CallPeerMediaHandler</tt> instance that
* this peer uses for media related tips and tricks.
*/
public U getMediaHandler()
{
return mediaHandler;
}
/**
* Returns a unique identifier representing this peer.
*
* @return an identifier representing this call peer.
*/
public String getPeerID()
{
return peerID;
}
/**
* Returns the protocol provider that this peer belongs to.
*
* @return a reference to the <tt>ProtocolProviderService</tt> that this
* peer belongs to.
*/
@Override
public V getProtocolProvider()
{
return protocolProvider;
}
/**
* Determines whether this <tt>CallPeer</tt> is participating in a telephony
* conference organized by the local user/peer utilizing the Jitsi
* Videobridge server-side technology.
*
* @return <tt>true</tt> if this <tt>CallPeer</tt> is participating in a
* telephony conference organized by the local user/peer utilizing the Jitsi
* Videobridge server-side technology; otherwise, <tt>false</tt>
*/
public final boolean isJitsiVideobridge()
{
Call call = getCall();
if (call != null)
{
CallConference conference = call.getConference();
if (conference != null)
return conference.isJitsiVideobridge();
}
return false;
}
/**
* Determines whether we are currently streaming video toward whoever this
* <tt>MediaAwareCallPeer</tt> represents.
*
* @return <tt>true</tt> if we are currently streaming video toward this
* <tt>CallPeer</tt> and <tt>false</tt> otherwise.
*/
public boolean isLocalVideoStreaming()
{
return getMediaHandler().isLocalVideoTransmissionEnabled();
}
/**
* Determines whether the audio stream (if any) being sent to this
* peer is mute.
*
* @return <tt>true</tt> if an audio stream is being sent to this
* peer and it is currently mute; <tt>false</tt>, otherwise
*/
@Override
public boolean isMute()
{
return getMediaHandler().isMute();
}
/**
* Logs <tt>message</tt> and <tt>cause</tt> and sets this <tt>peer</tt>'s
* state to <tt>CallPeerState.FAILED</tt>
*
* @param message a message to log and display to the user.
* @param throwable the exception that cause the error we are logging
*/
public void logAndFail(String message, Throwable throwable)
{
logger.error(message, throwable);
setState(CallPeerState.FAILED, message);
}
/**
* Updates the state of this <tt>CallPeer</tt> to match the locally-on-hold
* status of our media handler.
*/
public void reevalLocalHoldStatus()
{
CallPeerState state = getState();
boolean locallyOnHold = getMediaHandler().isLocallyOnHold();
if (CallPeerState.ON_HOLD_LOCALLY.equals(state))
{
if (!locallyOnHold)
setState(CallPeerState.CONNECTED);
}
else if (CallPeerState.ON_HOLD_MUTUALLY.equals(state))
{
if (!locallyOnHold)
setState(CallPeerState.ON_HOLD_REMOTELY);
}
else if (CallPeerState.ON_HOLD_REMOTELY.equals(state))
{
if (locallyOnHold)
setState(CallPeerState.ON_HOLD_MUTUALLY);
}
else if (locallyOnHold)
{
setState(CallPeerState.ON_HOLD_LOCALLY);
}
}
/**
* Updates the state of this <tt>CallPeer</tt> to match the remotely-on-hold
* status of our media handler.
*/
public void reevalRemoteHoldStatus()
{
boolean remotelyOnHold = getMediaHandler().isRemotelyOnHold();
CallPeerState state = getState();
if (CallPeerState.ON_HOLD_LOCALLY.equals(state))
{
if (remotelyOnHold)
setState(CallPeerState.ON_HOLD_MUTUALLY);
}
else if (CallPeerState.ON_HOLD_MUTUALLY.equals(state))
{
if (!remotelyOnHold)
setState(CallPeerState.ON_HOLD_LOCALLY);
}
else if (CallPeerState.ON_HOLD_REMOTELY.equals(state))
{
if (!remotelyOnHold)
setState(CallPeerState.CONNECTED);
}
else if (remotelyOnHold)
{
setState(CallPeerState.ON_HOLD_REMOTELY);
}
}
/**
* Removes a specific <tt>ConferenceMembersSoundLevelListener</tt> of the
* list of listeners interested in and notified about changes in conference
* members sound level.
*
* @param listener the <tt>ConferenceMembersSoundLevelListener</tt> to
* remove
*/
public void removeConferenceMembersSoundLevelListener(
ConferenceMembersSoundLevelListener listener)
{
synchronized (conferenceMembersSoundLevelListeners)
{
if (conferenceMembersSoundLevelListeners.remove(listener)
&& (conferenceMembersSoundLevelListeners.size() == 0))
{
// if this was the last listener then we also remove ourselves
// as a CSRC audio level listener from the handler so that we
// don't have to create new events and maps for something no one
// is interested in.
getMediaHandler().setCsrcAudioLevelListener(null);
}
}
}
/**
* Removes a specific <tt>SoundLevelListener</tt> of the list of
* listeners interested in and notified about changes in stream sound level
* related information.
*
* @param listener the <tt>SoundLevelListener</tt> to remove
*/
public void removeStreamSoundLevelListener(SoundLevelListener listener)
{
synchronized (streamSoundLevelListenersSyncRoot)
{
/*
* Implement streamAudioLevelListeners as a copy-on-write storage so
* that iterators over it can iterate over it without
* ConcurrentModificationExceptions.
*/
if (streamSoundLevelListeners != null)
{
streamSoundLevelListeners
= new ArrayList<SoundLevelListener>(
streamSoundLevelListeners);
if (streamSoundLevelListeners.remove(listener)
&& streamSoundLevelListeners.isEmpty())
streamSoundLevelListeners = null;
}
if ((streamSoundLevelListeners == null)
|| streamSoundLevelListeners.isEmpty())
{
// if this was the last listener then we also need to remove
// ourselves as an audio level so that audio levels would only
// be calculated if anyone is interested in receiving them.
getMediaHandler().setStreamAudioLevelListener(null);
}
}
}
/**
* Removes a specific <tt>PropertyChangeListener</tt> from the list of
* listeners which get notified when the properties (e.g.
* LOCAL_VIDEO_STREAMING) associated with this <tt>CallPeer</tt> change
* their values.
*
* @param listener the <tt>PropertyChangeListener</tt> to no longer be
* notified when the properties associated with the specified <tt>Call</tt>
* change their values
*/
public void removeVideoPropertyChangeListener(
PropertyChangeListener listener)
{
if (listener != null)
synchronized (videoPropertyChangeListeners)
{
/*
* The video is part of the media-related functionality and thus
* it is the responsibility of mediaHandler. So we're listening
* to mediaHandler for video-related property changes and w're
* re-firing them as originating from this instance. Make sure
* that we're not listening to mediaHandler if noone is
* interested in video-related property changes originating from
* this instance.
*/
if (videoPropertyChangeListeners.remove(listener)
&& videoPropertyChangeListeners.isEmpty()
&& (mediaHandlerPropertyChangeListener != null))
{
// getMediaHandler()
// .removePropertyChangeListener(
// mediaHandlerPropertyChangeListener);
mediaHandlerPropertyChangeListener = null;
}
}
}
/**
* Sets the security message associated with a failure/warning or
* information coming from the encryption protocol.
*
* @param messageType the type of the message.
* @param i18nMessage the message
* @param severity severity level
*/
public void securityMessageReceived(
String messageType,
String i18nMessage,
int severity)
{
fireCallPeerSecurityMessageEvent(messageType,
i18nMessage,
severity);
}
/**
* Indicates that the other party has timeouted replying to our
* offer to secure the connection.
*
* @param mediaType the <tt>MediaType</tt> of the call session
* @param sender the security controller that caused the event
*/
public void securityNegotiationStarted(
MediaType mediaType,
SrtpControl sender)
{
fireCallPeerSecurityNegotiationStartedEvent(
new CallPeerSecurityNegotiationStartedEvent(
this,
toSessionType(mediaType),
sender));
}
/**
* Indicates that the other party has timeouted replying to our
* offer to secure the connection.
*
* @param mediaType the <tt>MediaType</tt> of the call session
*/
public void securityTimeout(MediaType mediaType)
{
fireCallPeerSecurityTimeoutEvent(
new CallPeerSecurityTimeoutEvent(
this,
toSessionType(mediaType)));
}
/**
* Sets the security status to OFF for this call peer.
*
* @param mediaType the <tt>MediaType</tt> of the call session
*/
public void securityTurnedOff(MediaType mediaType)
{
// If this event has been triggered because of a call end event and the
// call is already ended we don't need to alert the user for
// security off.
if((call != null) && !call.getCallState().equals(CallState.CALL_ENDED))
{
fireCallPeerSecurityOffEvent(
new CallPeerSecurityOffEvent(
this,
toSessionType(mediaType)));
}
}
/**
* Sets the security status to ON for this call peer.
*
* @param mediaType the <tt>MediaType</tt> of the call session
* @param cipher the cipher
* @param sender the security controller that caused the event
*/
public void securityTurnedOn(
MediaType mediaType,
String cipher,
SrtpControl sender)
{
getMediaHandler().startSrtpMultistream(sender);
fireCallPeerSecurityOnEvent(
new CallPeerSecurityOnEvent(
this,
toSessionType(mediaType),
cipher,
sender));
}
/**
* Sets the call containing this peer.
*
* @param call the call that this call peer is participating in.
*/
public void setCall(T call)
{
this.call = call;
}
/**
* Sets the byte array containing an image representation (photo or picture)
* of the call peer.
*
* @param image a byte array containing the image
*/
public void setImage(byte[] image)
{
byte[] oldImage = getImage();
this.image = image;
//Fire the Event
fireCallPeerChangeEvent(
CallPeerChangeEvent.CALL_PEER_IMAGE_CHANGE,
oldImage,
image);
}
/**
* Modifies the local media setup to reflect the requested setting for the
* streaming of the local video and then re-invites the peer represented by
* this class using a corresponding SDP description..
*
* @param allowed <tt>true</tt> if local video transmission is allowed and
* <tt>false</tt> otherwise.
*
* @throws OperationFailedException if video initialization fails.
*/
public void setLocalVideoAllowed(boolean allowed)
throws OperationFailedException
{
CallPeerMediaHandler<?> mediaHandler = getMediaHandler();
if(mediaHandler.isLocalVideoTransmissionEnabled() != allowed)
{
// Modify the local media setup to reflect the requested setting for
// the streaming of the local video.
mediaHandler.setLocalVideoTransmissionEnabled(allowed);
}
}
/**
* Sets a reference to the <tt>CallPeerMediaHandler</tt> used by this
* peer. The media handler class handles all media management for a single
* <tt>CallPeer</tt>. This includes initializing and configuring streams,
* generating SDP, handling ICE, etc. One instance of <tt>CallPeer</tt>
* always corresponds to exactly one instance of
* <tt>CallPeerMediaHandler</tt> and both classes are only separated for
* reasons of readability.
*
* @param mediaHandler a reference to the <tt>CallPeerMediaHandler</tt>
* instance that this peer uses for media related tips and tricks.
*/
protected void setMediaHandler(U mediaHandler)
{
this.mediaHandler = mediaHandler;
}
/**
* Sets the mute property for this call peer.
*
* @param newMuteValue the new value of the mute property for this call peer
*/
@Override
public void setMute(boolean newMuteValue)
{
getMediaHandler().setMute(newMuteValue);
super.setMute(newMuteValue);
}
/**
* Sets the String that serves as a unique identifier of this
* CallPeer.
* @param peerID the ID of this call peer.
*/
public void setPeerID(String peerID)
{
this.peerID = peerID;
}
/**
* Overrides the parent set state method in order to make sure that we
* close our media handler whenever we enter a disconnected state.
*
* @param newState the <tt>CallPeerState</tt> that we are about to enter and
* that we pass to our predecessor.
* @param reason a reason phrase explaining the state (e.g. if newState
* indicates a failure) and that we pass to our predecessor.
* @param reasonCode the code for the reason of the state change.
*/
@Override
public void setState(CallPeerState newState, String reason, int reasonCode)
{
// synchronized to mediaHandler if there are currently jobs of
// initializing, configuring and starting streams (method processAnswer
// of CallPeerMediaHandler) we won't set and fire the current state
// to Disconnected. Before closing the mediaHandler is setting the state
// in order to deliver states as quick as possible.
CallPeerMediaHandler<?> mediaHandler = getMediaHandler();
synchronized(mediaHandler)
{
try
{
super.setState(newState, reason, reasonCode);
}
finally
{
// make sure whatever happens to close the media
if (CallPeerState.DISCONNECTED.equals(newState)
|| CallPeerState.FAILED.equals(newState))
mediaHandler.close();
}
}
}
/**
* Returns the last <tt>ConferenceInfoDocument</tt> sent by us to this
* <tt>CallPeer</tt>. It is a document with state <tt>full</tt>
* @return the last <tt>ConferenceInfoDocument</tt> sent by us to this
* <tt>CallPeer</tt>. It is a document with state <tt>full</tt>
*/
public ConferenceInfoDocument getLastConferenceInfoSent()
{
return lastConferenceInfoSent;
}
/**
* Sets the last <tt>ConferenceInfoDocument</tt> sent by us to this
* <tt>CallPeer</tt>.
* @param confInfo the document to set.
*/
public void setLastConferenceInfoSent(ConferenceInfoDocument confInfo)
{
lastConferenceInfoSent = confInfo;
}
/**
* Gets the time (as obtained by <tt>System.currentTimeMillis()</tt>)
* at which we last sent a <tt>ConferenceInfoDocument</tt> to this
* <tt>CallPeer</tt>.
* @return the time (as obtained by <tt>System.currentTimeMillis()</tt>)
* at which we last sent a <tt>ConferenceInfoDocument</tt> to this
* <tt>CallPeer</tt>.
*/
public long getLastConferenceInfoSentTimestamp()
{
return lastConferenceInfoSentTimestamp;
}
/**
* Sets the time (as obtained by <tt>System.currentTimeMillis()</tt>)
* at which we last sent a <tt>ConferenceInfoDocument</tt> to this
* <tt>CallPeer</tt>.
* @param newTimestamp the time to set
*/
public void setLastConferenceInfoSentTimestamp(long newTimestamp)
{
lastConferenceInfoSentTimestamp = newTimestamp;
}
/**
* Gets the last <tt>ConferenceInfoDocument</tt> sent to us by this
* <tt>CallPeer</tt>.
* @return the last <tt>ConferenceInfoDocument</tt> sent to us by this
* <tt>CallPeer</tt>.
*/
public ConferenceInfoDocument getLastConferenceInfoReceived()
{
return lastConferenceInfoReceived;
}
/**
* Gets the last <tt>ConferenceInfoDocument</tt> sent to us by this
* <tt>CallPeer</tt>.
* @return the last <tt>ConferenceInfoDocument</tt> sent to us by this
* <tt>CallPeer</tt>.
*/
public void setLastConferenceInfoReceived(ConferenceInfoDocument confInfo)
{
lastConferenceInfoReceived = confInfo;
}
/**
* Gets the <tt>version</tt> of the last <tt>ConferenceInfoDocument</tt>
* sent to us by this <tt>CallPeer</tt>, or -1 if we haven't (yet) received
* a <tt>ConferenceInformationDocument</tt> from this <tt>CallPeer</tt>.
* @return
*/
public int getLastConferenceInfoReceivedVersion()
{
return (lastConferenceInfoReceived == null)
? -1
: lastConferenceInfoReceived.getVersion();
}
/**
* Gets the <tt>String</tt> to be used for this <tt>CallPeer</tt> when
* we describe it in a <tt>ConferenceInfoDocument</tt> (e.g. the
* <tt>entity</tt> key attribute which to use for the <tt>user</tt>
* element corresponding to this <tt>CallPeer</tt>)
*
* @return the <tt>String</tt> to be used for this <tt>CallPeer</tt> when
* we describe it in a <tt>ConferenceInfoDocument</tt> (e.g. the
* <tt>entity</tt> key attribute which to use for the <tt>user</tt>
* element corresponding to this <tt>CallPeer</tt>)
*/
public abstract String getEntity();
/**
* Check whether a conference-info document is scheduled to be sent to
* this <tt>CallPeer</tt> (i.e. there is a thread which will eventually
* (after sleeping a certain amount of time) trigger a document to be sent)
* @return <tt>true</tt> if there is a conference-info document scheduled
* to be sent to this <tt>CallPeer</tt> and <tt>false</tt> otherwise.
*/
public boolean isConfInfoScheduled()
{
synchronized (confInfoScheduledSyncRoot)
{
return confInfoScheduled;
}
}
/**
* Sets the property which indicates whether a conference-info document
* is scheduled to be sent to this <tt>CallPeer</tt>.
* @param confInfoScheduled
*/
public void setConfInfoScheduled(boolean confInfoScheduled)
{
synchronized (confInfoScheduledSyncRoot)
{
this.confInfoScheduled = confInfoScheduled;
}
}
/**
* Returns the direction of the session for media of type <tt>mediaType</tt>
* that we have with this <tt>CallPeer</tt>. This is the direction of the
* session negotiated in the signaling protocol, and it may or may not
* coincide with the direction of the media stream.
* For example, if we are the focus of a videobridge conference and another
* peer is sending video to us, we have a <tt>RECVONLY</tt> video stream,
* but <tt>SENDONLY</tt> or <tt>SENDRECV</tt> (Jingle) sessions with the
* rest of the conference members.
* Should always return non-null.
*
* @param mediaType the <tt>MediaType</tt> to use
* @return Returns the direction of the session for media of type
* <tt>mediaType</tt> that we have with this <tt>CallPeer</tt>.
*/
public abstract MediaDirection getDirection(MediaType mediaType);
/**
* {@inheritDoc}
*
* When a <tt>ConferenceMember</tt> is removed from a conference with a
* Jitsi-videobridge, an RTCP BYE packet is not always sent. Therefore,
* if the <tt>ConferenceMember</tt> had an associated video SSRC, the stream
* isn't be removed until it times out, leaving a blank video container in
* the interface for a few seconds.
* TODO: This works around the problem by removing the
* <tt>ConferenceMember</tt>'s <tt>ReceiveStream</tt> when the
* <tt>ConferenceMember</tt> is removed. The proper solution is to ensure
* that RTCP BYEs are sent whenever necessary, and when it is deployed this
* code should be removed.
*
* @param conferenceMember a <tt>ConferenceMember</tt> to be removed from
* the list of <tt>ConferenceMember</tt> reported by this peer. If the
* specified <tt>ConferenceMember</tt> is no contained in the list, no event
*/
@Override
public void removeConferenceMember(ConferenceMember conferenceMember)
{
MediaStream videoStream = getMediaHandler().getStream(MediaType.VIDEO);
if (videoStream != null)
videoStream.removeReceiveStreamForSsrc(
conferenceMember.getVideoSsrc());
super.removeConferenceMember(conferenceMember);
}
/**
* Converts a specific <tt>MediaType</tt> into a <tt>sessionType</tt> value
* in the terms of the <tt>CallPeerSecurityStatusEvent</tt> class.
*
* @param mediaType the <tt>MediaType</tt> to be converted
* @return the <tt>sessionType</tt> value in the terms of the
* <tt>CallPeerSecurityStatusEvent</tt> class that is equivalent to the
* specified <tt>mediaType</tt>
*/
private static int toSessionType(MediaType mediaType)
{
switch (mediaType)
{
case AUDIO:
return CallPeerSecurityStatusEvent.AUDIO_SESSION;
case VIDEO:
return CallPeerSecurityStatusEvent.VIDEO_SESSION;
default:
throw new IllegalArgumentException("mediaType");
}
}
}