Skip to content
Snippets Groups Projects
MediaStreamImpl.java 99.6 KiB
Newer Older
  • Learn to ignore specific revisions
  • /*
     * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
     *
     * Distributable under LGPL license.
     * See terms of license at gnu.org.
     */
    package org.jitsi.impl.neomedia;
    
    
    import java.beans.*;
    
    import java.io.*;
    import java.net.*;
    import java.util.*;
    
    import javax.media.*;
    import javax.media.control.*;
    import javax.media.format.*;
    import javax.media.protocol.*;
    import javax.media.rtp.*;
    import javax.media.rtp.event.*;
    import javax.media.rtp.rtcp.*;
    
    import org.jitsi.impl.neomedia.device.*;
    import org.jitsi.impl.neomedia.format.*;
    
    import org.jitsi.impl.neomedia.protocol.*;
    
    import org.jitsi.impl.neomedia.transform.*;
    import org.jitsi.impl.neomedia.transform.csrc.*;
    import org.jitsi.impl.neomedia.transform.dtmf.*;
    import org.jitsi.impl.neomedia.transform.rtcp.*;
    import org.jitsi.impl.neomedia.transform.zrtp.*;
    import org.jitsi.service.neomedia.*;
    
    import org.jitsi.service.neomedia.control.*;
    
    import org.jitsi.service.neomedia.device.*;
    import org.jitsi.service.neomedia.format.*;
    import org.jitsi.util.*;
    
    /**
     * Implements <tt>MediaStream</tt> using JMF.
     *
     * @author Lyubomir Marinov
     * @author Emil Ivov
     * @author Sebastien Vincent
     */
    public class MediaStreamImpl
        extends AbstractMediaStream
        implements ReceiveStreamListener,
                   SendStreamListener,
                   SessionListener,
                   RemoteListener
    {
        /**
         * The <tt>Logger</tt> used by the <tt>MediaStreamImpl</tt> class and its
         * instances for logging output.
         */
        private static final Logger logger
            = Logger.getLogger(MediaStreamImpl.class);
    
        /**
         * The name of the property indicating the length of our receive buffer.
         */
        protected static final String PROPERTY_NAME_RECEIVE_BUFFER_LENGTH
    
            = "net.java.sip.communicator.impl.neomedia.RECEIVE_BUFFER_LENGTH";
    
    
        /**
         * The session with the <tt>MediaDevice</tt> this instance uses for both
         * capture and playback of media.
         */
        private MediaDeviceSession deviceSession;
    
        /**
         * The <tt>PropertyChangeListener</tt> which listens to
         * {@link #deviceSession} and changes in the values of its
         * {@link MediaDeviceSession#OUTPUT_DATA_SOURCE} property.
         */
        private final PropertyChangeListener deviceSessionPropertyChangeListener
            = new PropertyChangeListener()
            {
                public void propertyChange(PropertyChangeEvent event)
                {
                    String propertyName = event.getPropertyName();
    
                    if (MediaDeviceSession.OUTPUT_DATA_SOURCE.equals(propertyName))
                        deviceSessionOutputDataSourceChanged();
                    else if (MediaDeviceSession.SSRC_LIST.equals(propertyName))
                        deviceSessionSsrcListChanged(event);
                }
            };
    
        /**
         * The <tt>MediaDirection</tt> in which this <tt>MediaStream</tt> is allowed
         * to stream media.
         */
        private MediaDirection direction;
    
        /**
         * The <tt>Map</tt> of associations in this <tt>MediaStream</tt> and the
         * <tt>RTPManager</tt> it utilizes of (dynamic) RTP payload types to
         * <tt>MediaFormat</tt>s.
         */
        private final Map<Byte, MediaFormat> dynamicRTPPayloadTypes
            = new HashMap<Byte, MediaFormat>();
    
        /**
         * The <tt>ReceiveStream</tt>s this instance plays back on its associated
         * <tt>MediaDevice</tt>.
         */
        private final List<ReceiveStream> receiveStreams
            = new LinkedList<ReceiveStream>();
    
        /**
         * The <tt>RTPConnector</tt> through which this instance sends and receives
         * RTP and RTCP traffic. The instance is a <tt>TransformConnector</tt> in
         * order to also enable packet transformations.
         */
        private AbstractRTPConnector rtpConnector;
    
        /**
         * The one and only <tt>MediaStreamTarget</tt> this instance has added as a
         * target in {@link #rtpConnector}.
         */
        private MediaStreamTarget rtpConnectorTarget;
    
        /**
         * The <tt>RTPManager</tt> which utilizes {@link #rtpConnector} and sends
         * and receives RTP and RTCP traffic on behalf of this <tt>MediaStream</tt>.
         */
        private StreamRTPManager rtpManager;
    
        /**
         * The <tt>RTPTranslator</tt>, if any, which forwards RTP and RTCP traffic
         * between this and other <tt>MediaStream</tt>s.
         */
        private RTPTranslator rtpTranslator;
    
        /**
         * The indicator which determines whether {@link #createSendStreams()} has
         * been executed for {@link #rtpManager}. If <tt>true</tt>, the
         * <tt>SendStream</tt>s have to be recreated when the <tt>MediaDevice</tt>,
         * respectively the <tt>MediaDeviceSession</tt>, of this instance is
         * changed.
         */
        private boolean sendStreamsAreCreated = false;
    
        /**
         * The indicator which determines whether {@link #start()} has been called
         * on this <tt>MediaStream</tt> without {@link #stop()} or {@link #close()}.
         */
        private boolean started = false;
    
        /**
         * The <tt>MediaDirection</tt> in which this instance is started. For
         * example, {@link MediaDirection#SENDRECV} if this instances is both
         * sending and receiving data (e.g. RTP and RTCP) or
         * {@link MediaDirection#SENDONLY} if this instance is only sending data.
         */
        private MediaDirection startedDirection;
    
        /**
         * The SSRC identifiers of the party that we are exchanging media with.
         */
        private final Vector<Long> remoteSourceIDs = new Vector<Long>(1, 1);
    
        /**
         * Our own SSRC identifier.
         */
        private long localSourceID = -1;
    
        /**
         * The list of CSRC IDs contributing to the media that this
         * <tt>MediaStream</tt> is sending to its remote party.
         */
        private long[] localContributingSourceIDList = null;
    
        /**
         * The indicator which determines whether this <tt>MediaStream</tt> is set
         * to transmit "silence" instead of the actual media fed from its
         * <tt>MediaDevice</tt>.
         */
        private boolean mute = false;
    
        /**
         * The map of currently active <tt>RTPExtension</tt>s and the IDs that they
         * have been assigned for the lifetime of this <tt>MediaStream</tt>.
         */
        private final Map<Byte, RTPExtension> activeRTPExtensions
            = new Hashtable<Byte, RTPExtension>();
    
        /**
         * The engine that we are using in order to add CSRC lists in conference
         * calls, send CSRC sound levels, and handle incoming levels and CSRC lists.
         */
        private CsrcTransformEngine csrcEngine;
    
        /**
         * The <tt>SrtpControl</tt> which controls the SRTP functionality of this
         * <tt>MediaStream</tt>.
         */
        private final SrtpControl srtpControl;
    
        /**
         * Number of received sender reports. Used for logging and debugging only.
         */
        private long numberOfReceivedSenderReports = 0;
    
        /**
         * The minimum inter arrival jitter value the other party has reported.
         */
        private long maxRemoteInterArrivalJitter = 0;
    
        /**
         * The maximum inter arrival jitter value the other party has reported.
         */
        private long minRemoteInterArrivalJitter = -1;
    
        /**
         * Engine chain reading sent RTCP sender reports and stores/prints
         * statistics.
         */
        private StatisticsEngine statisticsEngine = null;
    
        /**
         * The MediaStreamStatsImpl object used to compute the statistics about
         * this MediaStreamImpl.
         */
        private MediaStreamStatsImpl mediaStreamStatsImpl;
    
        /**
         * Initializes a new <tt>MediaStreamImpl</tt> instance which will use the
         * specified <tt>MediaDevice</tt> for both capture and playback of media.
         * The new instance will not have an associated <tt>StreamConnector</tt> and
         * it must be set later for the new instance to be able to exchange media
         * with a remote peer.
         *
         * @param device the <tt>MediaDevice</tt> the new instance is to use for
         * both capture and playback of media
         * @param srtpControl an existing control instance to control the SRTP
         * operations
         */
        public MediaStreamImpl(MediaDevice device, SrtpControl srtpControl)
        {
            this(null, device, srtpControl);
        }
    
        /**
         * Initializes a new <tt>MediaStreamImpl</tt> instance which will use the
         * specified <tt>MediaDevice</tt> for both capture and playback of media
         * exchanged via the specified <tt>StreamConnector</tt>.
         *
         * @param connector the <tt>StreamConnector</tt> the new instance is to use
         * for sending and receiving media or <tt>null</tt> if the
         * <tt>StreamConnector</tt> of the new instance is to not be set at
         * initialization time but specified later on
         * @param device the <tt>MediaDevice</tt> the new instance is to use for
         * both capture and playback of media exchanged via the specified
         * <tt>StreamConnector</tt>
         * @param srtpControl an existing control instance to control the ZRTP
         * operations or <tt>null</tt> if a new control instance is to be created by
         * the new <tt>MediaStreamImpl</tt>
         */
        public MediaStreamImpl(
                StreamConnector connector,
                MediaDevice device,
                SrtpControl srtpControl)
        {
    
            if (device != null)
            {
                /*
                 * XXX Set the device early in order to make sure that it is of the
                 * right type because we do not support just about any MediaDevice
                 * yet.
                 */
                setDevice(device);
            }
    
    
            // TODO Add option to disable ZRTP, e.g. by implementing a NullControl.
            // If you change the default behavior (initiates a ZrtpControlImpl if
            // the srtpControl attribute is null), please accordingly modify the
            // CallPeerMediaHandler.initStream function.
            this.srtpControl
                    = (srtpControl == null)
    
                        ? NeomediaServiceUtils.getMediaServiceImpl()
    
                                .createZrtpControl()
                        : srtpControl;
    
            if (connector != null)
                setConnector(connector);
    
            this.mediaStreamStatsImpl = new MediaStreamStatsImpl(this);
    
    
            if (logger.isTraceEnabled())
            {
                logger.trace(
                        "Created "
                            + getClass().getSimpleName()
                            + " with hashCode "
                            + hashCode());
            }
    
        }
    
        /**
         * Performs any optional configuration on a specific
         * <tt>RTPConnectorOuputStream</tt> of an <tt>RTPManager</tt> to be used by
         * this <tt>MediaStreamImpl</tt>. Allows extenders to override.
         *
         * @param dataOutputStream the <tt>RTPConnectorOutputStream</tt> to be used
         * by an <tt>RTPManager</tt> of this <tt>MediaStreamImpl</tt> and to be
         * configured
         */
        protected void configureDataOutputStream(
                RTPConnectorOutputStream dataOutputStream)
        {
            dataOutputStream.setPriority(getPriority());
        }
    
        /**
         * Performs any optional configuration on a specific
         * <tt>RTPConnectorInputStream</tt> of an <tt>RTPManager</tt> to be used by
         * this <tt>MediaStreamImpl</tt>. Allows extenders to override.
         *
         * @param dataInputStream the <tt>RTPConnectorInputStream</tt> to be used
         * by an <tt>RTPManager</tt> of this <tt>MediaStreamImpl</tt> and to be
         * configured
         */
        protected void configureDataInputStream(
                RTPConnectorInputStream dataInputStream)
        {
            dataInputStream.setPriority(getPriority());
        }
    
        /**
         * Performs any optional configuration on the <tt>BufferControl</tt> of the
         * specified <tt>RTPManager</tt> which is to be used as the
         * <tt>RTPManager</tt> of this <tt>MediaStreamImpl</tt>. Allows extenders to
         * override.
         *
         * @param rtpManager the <tt>RTPManager</tt> which is to be used by this
         * <tt>MediaStreamImpl</tt>
         * @param bufferControl the <tt>BufferControl</tt> of <tt>rtpManager</tt> on
         * which any optional configuration is to be performed
         */
        protected void configureRTPManagerBufferControl(
                StreamRTPManager rtpManager,
                BufferControl bufferControl)
        {
        }
    
        /**
         * Creates a chain of transform engines for use with this stream. Note
         * that this is the only place where the <tt>TransformEngineChain</tt> is
         * and should be manipulated to avoid problems with the order of the
         * transformers.
         *
         * @return the <tt>TransformEngineChain</tt> that this stream should be
         * using.
         */
        private TransformEngineChain createTransformEngineChain()
        {
            ArrayList<TransformEngine> engineChain
                = new ArrayList<TransformEngine>(4);
    
            // CSRCs and audio levels
            if (csrcEngine == null)
                csrcEngine = new CsrcTransformEngine(this);
            engineChain.add(csrcEngine);
    
            // DTMF
            DtmfTransformEngine dtmfEngine = createDtmfTransformEngine();
    
            if (dtmfEngine != null)
                engineChain.add(dtmfEngine);
    
            // RTCP Statistics
            if (statisticsEngine == null)
                statisticsEngine = new StatisticsEngine(this);
            engineChain.add(statisticsEngine);
    
            // SRTP
            engineChain.add(srtpControl.getTransformEngine());
    
            return
                new TransformEngineChain(
                        engineChain.toArray(
                                new TransformEngine[engineChain.size()]));
        }
    
        /**
         * A stub that allows audio oriented streams to create and keep a reference
         * to a <tt>DtmfTransformEngine</tt>.
         *
         * @return a <tt>DtmfTransformEngine</tt> if this is an audio oriented
         * stream and <tt>null</tt> otherwise.
         */
        protected DtmfTransformEngine createDtmfTransformEngine()
        {
            return null;
        }
    
        /**
         * Adds a new association in this <tt>MediaStream</tt> of the specified RTP
         * payload type with the specified <tt>MediaFormat</tt> in order to allow it
         * to report <tt>rtpPayloadType</tt> in RTP flows sending and receiving
         * media in <tt>format</tt>. Usually, <tt>rtpPayloadType</tt> will be in the
         * range of dynamic RTP payload types.
         *
         * @param rtpPayloadType the RTP payload type to be associated in this
         * <tt>MediaStream</tt> with the specified <tt>MediaFormat</tt>
         * @param format the <tt>MediaFormat</tt> to be associated in this
         * <tt>MediaStream</tt> with <tt>rtpPayloadType</tt>
         * @see MediaStream#addDynamicRTPPayloadType(byte, MediaFormat)
         */
        public void addDynamicRTPPayloadType(
                byte rtpPayloadType,
                MediaFormat format)
        {
            @SuppressWarnings("unchecked")
            MediaFormatImpl<? extends Format> mediaFormatImpl
                = (MediaFormatImpl<? extends Format>) format;
    
            synchronized (dynamicRTPPayloadTypes)
            {
                dynamicRTPPayloadTypes.put(Byte.valueOf(rtpPayloadType), format);
    
                if (rtpManager != null)
                    rtpManager.addFormat(
                            mediaFormatImpl.getFormat(),
                            rtpPayloadType);
            }
        }
    
        /**
         * Maps or updates the mapping between <tt>extensionID</tt> and
         * <tt>rtpExtension</tt>. If <tt>rtpExtension</tt>'s <tt>MediaDirection</tt>
         * attribute is set to <tt>INACTIVE</tt> the mapping is removed from the
         * local extensions table and the extension would not be transmitted or
         * handled by this stream's <tt>RTPConnector</tt>.
         *
         * @param extensionID the ID that is being mapped to <tt>rtpExtension</tt>
         * @param rtpExtension the <tt>RTPExtension</tt> that we are mapping.
         */
        public void addRTPExtension(byte extensionID, RTPExtension rtpExtension)
        {
            synchronized (activeRTPExtensions)
            {
                if(rtpExtension.getDirection() == MediaDirection.INACTIVE)
                    activeRTPExtensions.remove(extensionID);
                else
                    activeRTPExtensions.put(extensionID, rtpExtension);
            }
        }
    
    
        /**
         * Asserts that the state of this instance will remain consistent if a
         * specific <tt>MediaDirection</tt> (i.e. <tt>direction</tt>) and a
         * <tt>MediaDevice</tt> with a specific <tt>MediaDirection</tt> (i.e.
         * <tt>deviceDirection</tt>) are both set on this instance.
         *
         * @param direction the <tt>MediaDirection</tt> to validate against the
         * specified <tt>deviceDirection</tt>
         * @param deviceDirection the <tt>MediaDirection</tt> of a
         * <tt>MediaDevice</tt> to validate against the specified <tt>direction</tt>
         * @param illegalArgumentExceptionMessage the message of the
         * <tt>IllegalArgumentException</tt> to be thrown if the state of this
         * instance would've been compromised if <tt>direction</tt> and the
         * <tt>MediaDevice</tt> associated with <tt>deviceDirection</tt> were both
         * set on this instance
         * @throws IllegalArgumentException if the state of this instance would've
         * been compromised were both <tt>direction</tt> and the
         * <tt>MediaDevice</tt> associated with <tt>deviceDirection</tt> set on this
         * instance
         */
        private void assertDirection(
                MediaDirection direction,
                MediaDirection deviceDirection,
                String illegalArgumentExceptionMessage)
            throws IllegalArgumentException
        {
            if ((direction != null)
                    && !direction.and(deviceDirection).equals(direction))
                throw new IllegalArgumentException(illegalArgumentExceptionMessage);
        }
    
    
        /**
         * Returns a map containing all currently active <tt>RTPExtension</tt>s in
         * use by this stream.
         *
         * @return a map containing all currently active <tt>RTPExtension</tt>s in
         * use by this stream.
         */
        public Map<Byte, RTPExtension> getActiveRTPExtensions()
        {
            synchronized (activeRTPExtensions)
            {
                return new HashMap<Byte, RTPExtension>(activeRTPExtensions);
            }
        }
    
        /**
         * Returns the ID currently assigned to a specific RTP extension.
         *
         * @param rtpExtension the RTP extension to get the currently assigned ID of
         * @return the ID currently assigned to the specified RTP extension or
         * <tt>-1</tt> if no ID has been defined for this extension so far
         */
        public byte getActiveRTPExtensionID(RTPExtension rtpExtension)
        {
            synchronized (activeRTPExtensions)
            {
                Set<Map.Entry<Byte, RTPExtension>> extSet
                    = this.activeRTPExtensions.entrySet();
    
                for (Map.Entry<Byte, RTPExtension> entry : extSet)
                {
                    if (entry.getValue().equals(rtpExtension))
                        return entry.getKey();
                }
            }
    
            return -1;
        }
    
        /**
         * Returns the engine that is responsible for adding the list of CSRC
         * identifiers to outgoing RTP packets during a conference.
         *
         * @return the engine that is responsible for adding the list of CSRC
         * identifiers to outgoing RTP packets during a conference.
         */
        protected CsrcTransformEngine getCsrcEngine()
        {
            return csrcEngine;
        }
    
        /**
         * Releases the resources allocated by this instance in the course of its
         * execution and prepares it to be garbage collected.
         *
         * @see MediaStream#close()
         */
        public void close()
        {
            stop();
            closeSendStreams();
    
            srtpControl.cleanup();
    
            if (csrcEngine != null)
            {
                csrcEngine.close();
                csrcEngine = null;
            }
    
            if (rtpManager != null)
            {
                if (logger.isInfoEnabled())
                    printFlowStatistics(rtpManager);
    
                rtpManager.removeReceiveStreamListener(this);
                rtpManager.removeSendStreamListener(this);
                rtpManager.removeSessionListener(this);
                rtpManager.removeRemoteListener(this);
                try
                {
                    rtpManager.dispose();
                    rtpManager = null;
                }
                catch (Throwable t)
                {
                    if (t instanceof ThreadDeath)
                        throw (ThreadDeath) t;
    
                    /*
                     * Analysis of heap dumps and application logs suggests that
                     * RTPManager#dispose() may throw an exception after a
                     * NullPointerException has been thrown by SendStream#close() as
                     * documented in
                     * #stopSendStreams(Iterable<SendStream>, boolean). It is
                     * unknown at the time of this writing whether we can do
                     * anything to prevent the exception here but it is clear that,
                     * if we let it go through, we will not release at least one
                     * capture device (i.e. we will at least skip the
                     * MediaDeviceSession#close() bellow). For example, if the
                     * exception is thrown for the audio stream in a call, its
                     * capture device will not be released and any video stream will
                     * not get its #close() method called at all.
                     */
                    logger.error("Failed to dispose of RTPManager", t);
                }
            }
    
    
            /*
             * XXX Call AbstractRTPConnector#removeTargets() after
             * StreamRTPManager#dispose(). Otherwise, the latter will try to send an
             * RTCP BYE and there will be no targets to send it to.
             */
            if (rtpConnector != null)
                rtpConnector.removeTargets();
            rtpConnectorTarget = null;
    
    
            if (deviceSession != null)
                deviceSession.close();
        }
    
        /**
         * Closes the <tt>SendStream</tt>s this instance is sending to its remote
         * peer.
         */
        private void closeSendStreams()
        {
            stopSendStreams(true);
        }
    
        /**
         * Creates new <tt>SendStream</tt> instances for the streams of
         * {@link #deviceSession} through {@link #rtpManager}.
         */
        private void createSendStreams()
        {
            StreamRTPManager rtpManager = getRTPManager();
            MediaDeviceSession deviceSession = getDeviceSession();
            DataSource dataSource = deviceSession.getOutputDataSource();
            int streamCount;
    
            if (dataSource instanceof PushBufferDataSource)
            {
                PushBufferStream[] streams
                    = ((PushBufferDataSource) dataSource).getStreams();
    
                streamCount = (streams == null) ? 0 : streams.length;
            }
            else if (dataSource instanceof PushDataSource)
            {
                PushSourceStream[] streams
                    = ((PushDataSource) dataSource).getStreams();
    
                streamCount = (streams == null) ? 0 : streams.length;
            }
            else if (dataSource instanceof PullBufferDataSource)
            {
                PullBufferStream[] streams
                    = ((PullBufferDataSource) dataSource).getStreams();
    
                streamCount = (streams == null) ? 0 : streams.length;
            }
            else if (dataSource instanceof PullDataSource)
            {
                PullSourceStream[] streams
                    = ((PullDataSource) dataSource).getStreams();
    
                streamCount = (streams == null) ? 0 : streams.length;
            }
            else
                streamCount = (dataSource == null) ? 0 : 1;
    
            /*
             * XXX We came up with a scenario in our testing in which G.722 would
             * work fine for the first call since the start of the application and
             * then it would fail for subsequent calls, JMF would complain that the
             * G.722 RTP format is unknown to the RTPManager. Since
             * RTPManager#createSendStream(DataSource, int) is one of the cases in
             * which the formats registered with the RTPManager are necessary,
             * register them (again) just before we use them.
             */
            registerCustomCodecFormats(rtpManager);
    
            for (int streamIndex = 0; streamIndex < streamCount; streamIndex++)
            {
                try
                {
                    SendStream sendStream
                        = rtpManager.createSendStream(dataSource, streamIndex);
    
                    if (logger.isTraceEnabled())
                        logger
                            .trace(
                                "Created SendStream"
                                    + " with hashCode "
                                    + sendStream.hashCode()
                                    + " for "
                                    + toString(dataSource)
                                    + " and streamIndex "
                                    + streamIndex
                                    + " in RTPManager with hashCode "
                                    + rtpManager.hashCode());
    
                    long localSSRC = sendStream.getSSRC();
    
                    if (getLocalSourceID() != localSSRC)
                        setLocalSourceID(localSSRC);
                }
                catch (IOException ioe)
                {
                    logger
                        .error(
                            "Failed to create send stream for data source "
                                + dataSource
                                + " and stream index "
                                + streamIndex,
                            ioe);
                }
                catch (UnsupportedFormatException ufe)
                {
                    logger
                        .error(
                            "Failed to create send stream for data source "
                                + dataSource
                                + " and stream index "
                                + streamIndex
                                + " because of failed format "
                                + ufe.getFailedFormat(),
                            ufe);
                }
            }
            sendStreamsAreCreated = true;
    
            if (logger.isTraceEnabled())
            {
                @SuppressWarnings("unchecked")
                Vector<SendStream> sendStreams = rtpManager.getSendStreams();
                int sendStreamCount
                    = (sendStreams == null) ? 0 : sendStreams.size();
    
                logger
                    .trace(
                        "Total number of SendStreams in RTPManager with hashCode "
                            + rtpManager.hashCode()
                            + " is "
                            + sendStreamCount);
            }
        }
    
        /**
         * Notifies this <tt>MediaStream</tt> that the <tt>MediaDevice</tt> (and
         * respectively the <tt>MediaDeviceSession</tt> with it) which this instance
         * uses for capture and playback of media has been changed. Allows extenders
         * to override and provide additional processing of <tt>oldValue</tt> and
         * <tt>newValue</tt>.
         *
         * @param oldValue the <tt>MediaDeviceSession</tt> with the
         * <tt>MediaDevice</tt> this instance used work with
         * @param newValue the <tt>MediaDeviceSession</tt> with the
         * <tt>MediaDevice</tt> this instance is to work with
         */
        protected void deviceSessionChanged(
                MediaDeviceSession oldValue,
                MediaDeviceSession newValue)
        {
            recreateSendStreams();
        }
    
        /**
         * Notifies this instance that the output <tt>DataSource</tt> of its
         * <tt>MediaDeviceSession</tt> has changed. Recreates the
         * <tt>SendStream</tt>s of this instance as necessary so that it, for
         * example, continues streaming after the change if it was streaming before
         * the change.
         */
        private void deviceSessionOutputDataSourceChanged()
        {
            recreateSendStreams();
        }
    
        /**
         * Recalculates the list of CSRC identifiers that this <tt>MediaStream</tt>
         * needs to include in RTP packets bound to its interlocutor. The method
         * uses the list of SSRC identifiers currently handled by our device
         * (possibly a mixer), then removes the SSRC ID of this stream's
         * interlocutor. If this turns out to be the only SSRC currently in the list
         * we set the list of local CSRC identifiers to null since this is obviously
         * a non-conf call and we don't need to be advertising CSRC lists. If that's
         * not the case, we also add our own SSRC to the list of IDs and cache the
         * entire list.
         *
         * @param evt the <tt>PropetyChangeEvent</tt> containing the list of SSRC
         * identifiers handled by our device session before and after it changed.
         */
        private void deviceSessionSsrcListChanged(PropertyChangeEvent evt)
        {
            long[] ssrcArray = (long[])evt.getNewValue();
    
            // the list is empty
            if(ssrcArray == null)
            {
                this.localContributingSourceIDList = null;
                return;
            }
    
            int elementsToRemove = 0;
            Vector<Long> remoteSourceIDs = this.remoteSourceIDs;
    
            //in case of a conf call the mixer would return all SSRC IDs that are
            //currently contributing including this stream's counterpart. We need
            //to remove that last one since that's where we will be sending our
            //csrc list
            for(long csrc : ssrcArray)
                if (remoteSourceIDs.contains(csrc))
                    elementsToRemove ++;
    
            //we don't seem to be in a conf call since the list only contains the
            //SSRC id of the party that we are directly interacting with.
            if (elementsToRemove >= ssrcArray.length)
            {
                this.localContributingSourceIDList = null;
                return;
            }
    
            //prepare the new array. make it big enough to also add the local
            //SSRC id but do not make it bigger than 15 since that's the maximum
            //for RTP.
            int cc = Math.min(ssrcArray.length - elementsToRemove + 1, 15);
    
            long[] csrcArray = new long[cc];
    
            for (int i = 0,j = 0;
                    (i < ssrcArray.length) && (j < csrcArray.length - 1);
                    i++)
            {
                long ssrc = ssrcArray[i];
    
                if (!remoteSourceIDs.contains(ssrc))
                {
                    csrcArray[j] = ssrc;
                    j++;
                }
            }
    
            csrcArray[csrcArray.length - 1] = getLocalSourceID();
            this.localContributingSourceIDList = csrcArray;
        }
    
        /**
         * Sets the target of this <tt>MediaStream</tt> to which it is to send and
         * from which it is to receive data (e.g. RTP) and control data (e.g. RTCP).
         * In contrast to {@link #setTarget(MediaStreamTarget)}, sets the specified
         * <tt>target</tt> on this <tt>MediaStreamImpl</tt> even if its current
         * <tt>target</tt> is equal to the specified one.
         *
         * @param target the <tt>MediaStreamTarget</tt> describing the data
         * (e.g. RTP) and the control data (e.g. RTCP) locations to which this
         * <tt>MediaStream</tt> is to send and from which it is to receive
         * @see MediaStreamImpl#setTarget(MediaStreamTarget)
         */
        private void doSetTarget(MediaStreamTarget target)
        {
    
            InetSocketAddress newDataAddr;
            InetSocketAddress newControlAddr;
    
            if (target == null)
            {
                newDataAddr = null;
                newControlAddr = null;
            }
            else
            {
                newDataAddr = target.getDataAddress();
                newControlAddr = target.getControlAddress();
            }
    
            /*
             * Invoke AbstractRTPConnector#removeTargets() if the new value does
             * actually remove an RTP or RTCP target in comparison to the old value.
             * If the new value is equal to the oldValue or adds an RTP or RTCP
             * target (i.e. the old value does not specify the respective RTP or
             * RTCP target and the new value does), then removeTargets is
             * unnecessary and would've needlessly allowed a (tiny) interval of
             * (execution) time (between removeTargets and addTarget) without a
             * target.
             */
            if (rtpConnectorTarget != null)
    
                InetSocketAddress oldDataAddr = rtpConnectorTarget.getDataAddress();
                boolean removeTargets
                    = (oldDataAddr == null)
                        ? (newDataAddr != null)
                        : !oldDataAddr.equals(newDataAddr);
    
                if (!removeTargets)
                {
                    InetSocketAddress oldControlAddr
                        = rtpConnectorTarget.getControlAddress();
    
                    removeTargets
                        = (oldControlAddr == null)
                            ? (newControlAddr != null)
                            : !oldControlAddr.equals(newControlAddr);
                }
    
                if (removeTargets)
                {
                    rtpConnector.removeTargets();
                    rtpConnectorTarget = null;
                }
            }
    
            boolean targetIsSet;
    
                    InetAddress controlInetAddr;
                    int controlPort;
    
                    if (newControlAddr == null)
                    {
                        controlInetAddr = null;
                        controlPort = 0;
                    }
                    else
                    {
                        controlInetAddr = newControlAddr.getAddress();
                        controlPort = newControlAddr.getPort();
                    }
    
                    rtpConnector.addTarget(
    
                            new SessionAddress(
    
                                    newDataAddr.getAddress(), newDataAddr.getPort(),
                                    controlInetAddr, controlPort));
    
                    targetIsSet = true;
                }
                catch (IOException ioe)
                {
                    // TODO
                    targetIsSet = false;
                    logger.error("Failed to set target " + target, ioe);
                }
            }
            if (targetIsSet)
            {
                rtpConnectorTarget = target;
    
                if (logger.isTraceEnabled())
    
                {
                    logger.trace(
                            "Set target of " + getClass().getSimpleName()
                                + " with hashCode " + hashCode()
                                + " to " + target);
                }
    
            }
        }
    
        /**
         * Gets the <tt>MediaDevice</tt> that this stream uses to play back and
         * capture media.
         *
         * @return the <tt>MediaDevice</tt> that this stream uses to play back and
         * capture media
         * @see MediaStream#getDevice()
         */
        public AbstractMediaDevice getDevice()
        {
            return getDeviceSession().getDevice();
        }
    
    
        /**
         * Gets the <tt>MediaDirection</tt> of the <tt>device</tt> of this instance
         * if any or {@link MediaDirection#INACTIVE}.
         *
         * @return the <tt>MediaDirection</tt> of the <tt>device</tt> of this
         * instance if any or <tt>MediaDirection.INACTIVE</tt>
         */
        private MediaDirection getDeviceDirection()
        {
            MediaDeviceSession deviceSession = getDeviceSession();
    
            return
                (deviceSession == null)
                    ? MediaDirection.INACTIVE
                    : deviceSession.getDevice().getDirection();
        }
    
    
        /**
         * Gets the <tt>MediaDeviceSession</tt> which represents the work of this
         * <tt>MediaStream</tt> with its associated <tt>MediaDevice</tt>.
         *
         * @return the <tt>MediaDeviceSession</tt> which represents the work of this
         * <tt>MediaStream</tt> with its associated <tt>MediaDevice</tt>
         */
        public MediaDeviceSession getDeviceSession()
        {
            return deviceSession;
        }
    
        /**
         * Gets the direction in which this <tt>MediaStream</tt> is allowed to
         * stream media.
         *
         * @return the <tt>MediaDirection</tt> in which this <tt>MediaStream</tt> is
         * allowed to stream media
         * @see MediaStream#getDirection()
         */
        public MediaDirection getDirection()
        {
    
            return (direction == null) ? getDeviceDirection() : direction;
    
        }
    
        /**
         * Gets the existing associations in this <tt>MediaStream</tt> of RTP
         * payload types to <tt>MediaFormat</tt>s. The returned <tt>Map</tt>
         * only contains associations previously added in this instance with