Skip to content
Snippets Groups Projects
MediaStreamImpl.java 99.80 KiB
/*
 * 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()
    {
        /* Some statistics cannot be taken from the RTP manager and have to
         * be gathered from the ReceiveStream. We need to do this before
         * calling stop(). */
        if(logger.isInfoEnabled())
            printReceiveStreamStatistics();

        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;

        if (target == null)
            targetIsSet = true;
        else
        {
            try
            {
                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
     * {@link #addDynamicRTPPayloadType(byte, MediaFormat)} and not globally or
     * well-known associations reported by
     * {@link MediaFormat#getRTPPayloadType()}.
     *
     * @return a <tt>Map</tt> of RTP payload type expressed as <tt>Byte</tt> to
     * <tt>MediaFormat</tt> describing the existing (dynamic) associations in
     * this instance of RTP payload types to <tt>MediaFormat</tt>s. The
     * <tt>Map</tt> represents a snapshot of the existing associations at the
     * time of the <tt>getDynamicRTPPayloadTypes()</tt> method call and
     * modifications to it are not reflected on the internal storage
     * @see MediaStream#getDynamicRTPPayloadTypes()
     */
    public Map<Byte, MediaFormat> getDynamicRTPPayloadTypes()
    {
        synchronized (dynamicRTPPayloadTypes)
        {
            return new HashMap<Byte, MediaFormat>(dynamicRTPPayloadTypes);
        }
    }

    /**
     * Returns the payload type number that has been negotiated for the
     * specified <tt>encoding</tt> or <tt>-1</tt> if no payload type has been
     * negotiated for it. If multiple formats match the specified
     * <tt>encoding</tt>, then this method would return the first one it
     * encounters while iterating through the map.
     *
     * @param encoding the encoding whose payload type we are trying to obtain.
     *
     * @return the payload type number that has been negotiated for the
     * specified <tt>encoding</tt> or <tt>-1</tt> if no payload type has been
     * negotiated for it.
     */
    public byte getDynamicRTPPayloadType(String encoding)
    {
        synchronized (dynamicRTPPayloadTypes)
        {
            for (Map.Entry<Byte, MediaFormat> entry
                                        : dynamicRTPPayloadTypes.entrySet())
            {
                if (entry.getValue().getEncoding().equals(encoding))
                    return entry.getKey().byteValue();
            }
            return -1;
        }
    }

    /**
     * Gets the <tt>MediaFormat</tt> that this stream is currently transmitting
     * in.
     *
     * @return the <tt>MediaFormat</tt> that this stream is currently
     * transmitting in
     * @see MediaStream#getFormat()
     */
    public MediaFormat getFormat()
    {
        MediaDeviceSession deviceSession = getDeviceSession();

        return (deviceSession == null) ? null : deviceSession.getFormat();
    }

    /**
     * Gets the synchronization source (SSRC) identifier of the local peer or
     * <tt>-1</tt> if it is not yet known.
     *
     * @return  the synchronization source (SSRC) identifier of the local peer
     * or <tt>-1</tt> if it is not yet known
     * @see MediaStream#getLocalSourceID()
     */
    public long getLocalSourceID()
    {
        return this.localSourceID;
    }

    /**
     * Gets the address that this stream is sending RTCP traffic to.
     *
     * @return an <tt>InetSocketAddress</tt> instance indicating the address
     * that this stream is sending RTCP traffic to
     * @see MediaStream#getRemoteControlAddress()
     */
    public InetSocketAddress getRemoteControlAddress()
    {
        StreamConnector connector =
            (rtpConnector != null) ? rtpConnector.getConnector() : null;

        if(connector != null)
        {
            if(connector.getDataSocket() != null)
            {
                return (InetSocketAddress)connector.getControlSocket().
                    getRemoteSocketAddress();
            }
            else if(connector.getDataTCPSocket() != null)
            {
                return (InetSocketAddress)connector.getControlTCPSocket().
                    getRemoteSocketAddress();
            }
        }

        return null;
    }

    /**
     * Gets the address that this stream is sending RTP traffic to.
     *
     * @return an <tt>InetSocketAddress</tt> instance indicating the address
     * that this stream is sending RTP traffic to
     * @see MediaStream#getRemoteDataAddress()
     */
    public InetSocketAddress getRemoteDataAddress()
    {
        StreamConnector connector =
            (rtpConnector != null) ? rtpConnector.getConnector() : null;

        if(connector != null)
        {
            if(connector.getDataSocket() != null)
            {
                return (InetSocketAddress)connector.getDataSocket().
                    getRemoteSocketAddress();
            }
            else if(connector.getDataTCPSocket() != null)
            {
                return (InetSocketAddress)connector.getDataTCPSocket().
                    getRemoteSocketAddress();
            }
        }

        return null;
    }

    /**
     * Gets the local address that this stream is sending RTCP traffic from.
     *
     * @return an <tt>InetSocketAddress</tt> instance indicating the local
     * address that this stream is sending RTCP traffic from.
     */
    public InetSocketAddress getLocalControlAddress()
    {
        StreamConnector connector =
            (rtpConnector != null) ? rtpConnector.getConnector() : null;

        if(connector != null)
        {
            if(connector.getDataSocket() != null)
            {
                return (InetSocketAddress)connector.getControlSocket().
                    getLocalSocketAddress();
            }
            else if(connector.getDataTCPSocket() != null)
            {
                return (InetSocketAddress)connector.getControlTCPSocket().
                    getLocalSocketAddress();
            }
        }

        return null;
    }

    /**
     * Gets the local address that this stream is sending RTP traffic from.
     *
     * @return an <tt>InetSocketAddress</tt> instance indicating the local
     * address that this stream is sending RTP traffic from.
     */
    public InetSocketAddress getLocalDataAddress()
    {
        StreamConnector connector =
            (rtpConnector != null) ? rtpConnector.getConnector() : null;

        if(connector != null)
        {
            if(connector.getDataSocket() != null)
            {
                return (InetSocketAddress)connector.getDataSocket().
                    getLocalSocketAddress();
            }
            else if(connector.getDataTCPSocket() != null)
            {
                return (InetSocketAddress)connector.getDataTCPSocket().
                    getLocalSocketAddress();
            }
        }

        return null;
    }

    /**
     * Returns the transport protocol used by the streams.
     *
     * @return the transport protocol (UDP or TCP) used by the streams. null if
     * the stream connector is not instanciated.
     */
    public StreamConnector.Protocol getTransportProtocol()
    {
        StreamConnector connector =
            (rtpConnector != null) ? rtpConnector.getConnector() : null;

        if(connector == null)
        {
            return null;
        }

        return connector.getProtocol();
    }

    /**
     * {@inheritDoc}
     *
     * Returns the last element of {@link #getRemoteSourceIDs()} which may or
     * may not always be appropriate.
     *
     * @see MediaStream#getRemoteSourceID()
     */
    public long getRemoteSourceID()
    {
        return remoteSourceIDs.isEmpty() ? -1 : remoteSourceIDs.lastElement();
    }

    /**
     * Gets the synchronization source (SSRC) identifiers of the remote peer.
     *
     * @return the synchronization source (SSRC) identifiers of the remote peer
     */
    public List<Long> getRemoteSourceIDs()
    {
        /*
         * TODO Returning an unmodifiable view of remoteSourceIDs prevents
         * modifications of private state from the outside but it does not
         * prevent ConcurrentModificationException. 
         */
        return Collections.unmodifiableList(remoteSourceIDs);
    }

    /**
     * Gets the <tt>RTPConnector</tt> through which this instance sends and
     * receives RTP and RTCP traffic.
     *
     * @return the <tt>RTPConnector</tt> through which this instance sends and
     * receives RTP and RTCP traffic
     */
    protected AbstractRTPConnector getRTPConnector()
    {
        return rtpConnector;
    }

    /**
     * Gets the <tt>RTPManager</tt> instance which sends and receives RTP and
     * RTCP traffic on behalf of this <tt>MediaStream</tt>.
     *
     * @return the <tt>RTPManager</tt> instance which sends and receives RTP and
     * RTCP traffic on behalf of this <tt>MediaStream</tt>
     */
    public StreamRTPManager getRTPManager()
    {
        if (rtpManager == null)
        {
            RTPConnector rtpConnector = getRTPConnector();

            if (rtpConnector == null)
                throw new IllegalStateException("rtpConnector");

            rtpManager = new StreamRTPManager(rtpTranslator);

            registerCustomCodecFormats(rtpManager);

            rtpManager.addReceiveStreamListener(this);
            rtpManager.addSendStreamListener(this);
            rtpManager.addSessionListener(this);
            rtpManager.addRemoteListener(this);

            BufferControl bc
                = (BufferControl)
                    rtpManager.getControl(BufferControl.class.getName());
            if (bc != null)
                configureRTPManagerBufferControl(rtpManager, bc);

            rtpManager.initialize(rtpConnector);

            /*
             * JMF initializes the local SSRC upon #initialize(RTPConnector) so
             * now's the time to ask.
             */
            /*
             * As JMF keeps the SSRC as a signed int value, convert it to
             * unsigned.
             */
            setLocalSourceID(rtpManager.getLocalSSRC() & 0xFFFFFFFFL);
        }
        return rtpManager;
    }

    /**
     * Gets the <tt>SrtpControl</tt> which controls the SRTP of this stream.
     *
     * @return the <tt>SrtpControl</tt> which controls the SRTP of this stream
     */
    public SrtpControl getSrtpControl()
    {
        return srtpControl;
    }

    /**
     * Determines whether this <tt>MediaStream</tt> is set to transmit "silence"
     * instead of the media being fed from its <tt>MediaDevice</tt>. "Silence"
     * for video is understood as video data which is not the captured video
     * data and may represent, for example, a black image.
     *
     * @return <tt>true</tt> if this <tt>MediaStream</tt> is set to transmit
     * "silence" instead of the media fed from its <tt>MediaDevice</tt>;
     * <tt>false</tt>, otherwise
     * @see MediaStream#isMute()
     */
    public boolean isMute()
    {
        MediaDeviceSession deviceSession = getDeviceSession();

        return (deviceSession == null) ? mute : deviceSession.isMute();
    }

    /**
     * Determines whether {@link #start()} has been called on this
     * <tt>MediaStream</tt> without {@link #stop()} or {@link #close()}
     * afterwards.
     *
     * @return <tt>true</tt> if {@link #start()} has been called on this
     * <tt>MediaStream</tt> without {@link #stop()} or {@link #close()}
     * afterwards
     * @see MediaStream#isStarted()
     */
    public boolean isStarted()
    {
        return started;
    }

    /**
     * Recreates the <tt>SendStream</tt>s of this instance (i.e. of its
     * <tt>RTPManager</tt>) as necessary. For example, if there was no attempt
     * to create the <tt>SendStream</tt>s prior to the call, does nothing. If
     * they were created prior to the call, closes them and creates them again.
     * If they were not started prior to the call, does not start them after
     * recreating them.
     */
    private void recreateSendStreams()
    {
        if (sendStreamsAreCreated)
        {
            closeSendStreams();

            if ((getDeviceSession() != null) && (rtpManager != null))
            {
                if (MediaDirection.SENDONLY.equals(startedDirection)
                        || MediaDirection.SENDRECV.equals(startedDirection))
                    startSendStreams();
            }
        }
    }

    /**
     * Registers any custom JMF <tt>Format</tt>s with a specific
     * <tt>RTPManager</tt>. Extenders should override in order to register their
     * own customizations and should call back to this super implementation
     * during the execution of their override in order to register the
     * associations defined in this instance of (dynamic) RTP payload types to
     * <tt>MediaFormat</tt>s.
     *
     * @param rtpManager the <tt>RTPManager</tt> to register any custom JMF
     * <tt>Format</tt>s with
     */
    protected void registerCustomCodecFormats(StreamRTPManager rtpManager)
    {
        synchronized (dynamicRTPPayloadTypes)
        {
            for (Map.Entry<Byte, MediaFormat> dynamicRTPPayloadType
                    : dynamicRTPPayloadTypes.entrySet())
            {
                @SuppressWarnings("unchecked")
                MediaFormatImpl<? extends Format> mediaFormatImpl
                    = (MediaFormatImpl<? extends Format>)
                        dynamicRTPPayloadType.getValue();

                rtpManager.addFormat(
                        mediaFormatImpl.getFormat(),
                        dynamicRTPPayloadType.getKey());
            }
        }
    }

    /**
     * Notifies this <tt>MediaStream</tt> implementation that its
     * <tt>RTPConnector</tt> instance has changed from a specific old value to a
     * specific new value. Allows extenders to override and perform additional
     * processing after this <tt>MediaStream</tt> has changed its
     * <tt>RTPConnector</tt> instance.
     *
     * @param oldValue the <tt>RTPConnector</tt> of this <tt>MediaStream</tt>
     * implementation before it got changed to <tt>newValue</tt>
     * @param newValue the current <tt>RTPConnector</tt> of this
     * <tt>MediaStream</tt> which replaced <tt>oldValue</tt>
     */
    protected void rtpConnectorChanged(
            AbstractRTPConnector oldValue,
            AbstractRTPConnector newValue)
    {
        srtpControl.setConnector(newValue);

        if (newValue != null)
        {
            /*
             * Register the transform engines that we will be using in this
             * stream.
             */
            if(newValue instanceof RTPTransformUDPConnector)
                ((RTPTransformUDPConnector)newValue)
                    .setEngine(createTransformEngineChain());
            else if(newValue instanceof RTPTransformTCPConnector)
                ((RTPTransformTCPConnector)newValue)
                    .setEngine(createTransformEngineChain());

            if (rtpConnectorTarget != null)
                doSetTarget(rtpConnectorTarget);
        }
    }

    /**
     * Notifies this instance that its {@link #rtpConnector} has created a new
     * <tt>RTPConnectorInputStream</tt> either RTP or RTCP.
     *
     * @param inputStream the new <tt>RTPConnectorInputStream</tt> instance
     * created by the <tt>rtpConnector</tt> of this instance
     * @param data <tt>true</tt> if <tt>inputStream</tt> will be used for RTP
     * or <tt>false</tt> for RTCP
     */
    private void rtpConnectorInputStreamCreated(
            RTPConnectorInputStream inputStream,
            boolean data)
    {
        /*
         * TODO The following is a very ugly way to expose the
         * RTPConnectorInputStreams created by the rtpConnector of this
         * instance so they may be configured from outside the class hierarchy
         * (e.g. to invoke addDatagramPacketFilter). That's why the property in
         * use bellow is not defined as a well-known constant and is to be
         * considered internal and likely to be removed in a future revision.
         */
        try
        {
            firePropertyChange(
                    MediaStreamImpl.class.getName()
                        + ".rtpConnector."
                        + (data ? "data" : "control")
                        + "InputStream",
                    null,
                    inputStream);
        }
        catch (Throwable t)
        {
            if (t instanceof ThreadDeath)
                throw (ThreadDeath) t;
            else
                logger.error(t);
        }
    }

    /**
     * Sets the <tt>StreamConnector</tt> to be used by this instance for sending
     * and receiving media.
     *
     * @param connector the <tt>StreamConnector</tt> to be used by this instance
     * for sending and receiving media
     */
    public void setConnector(StreamConnector connector)
    {
        if (connector == null)
            throw new NullPointerException("connector");

        AbstractRTPConnector oldValue = rtpConnector;

        // Is the StreamConnector really changing?
        if ((oldValue != null) && (oldValue.getConnector() == connector))
            return;

        switch (connector.getProtocol())
        {
        case UDP:
            rtpConnector
                = new RTPTransformUDPConnector(connector)
                {
                    @Override
                    protected TransformUDPInputStream createControlInputStream()
                        throws IOException
                    {
                        TransformUDPInputStream s
                            = super.createControlInputStream();

                        rtpConnectorInputStreamCreated(s, false);
                        return s;
                    }

                    @Override
                    protected TransformUDPInputStream createDataInputStream()
                        throws IOException
                    {
                        TransformUDPInputStream s
                            = super.createDataInputStream();

                        rtpConnectorInputStreamCreated(s, true);
                        if (s != null)
                            configureDataInputStream(s);
                        return s;
                    }

                    @Override
                    protected TransformUDPOutputStream createDataOutputStream()
                        throws IOException
                    {
                        TransformUDPOutputStream s
                            = super.createDataOutputStream();

                        if (s != null)
                            configureDataOutputStream(s);
                        return s;
                    }
                };
            break;
        case TCP:
            rtpConnector
                = new RTPTransformTCPConnector(connector)
                {
                    @Override
                    protected TransformTCPInputStream createControlInputStream()
                        throws IOException
                    {
                        TransformTCPInputStream s
                            = super.createControlInputStream();

                        rtpConnectorInputStreamCreated(s, false);
                        return s;
                    }

                    @Override
                    protected TransformTCPInputStream createDataInputStream()
                        throws IOException
                    {
                        TransformTCPInputStream s
                            = super.createDataInputStream();

                        rtpConnectorInputStreamCreated(s, true);
                        if (s != null)
                            configureDataInputStream(s);
                        return s;
                    }

                    @Override
                    protected TransformTCPOutputStream createDataOutputStream()
                        throws IOException
                    {
                        TransformTCPOutputStream s
                            = super.createDataOutputStream();

                        if (s != null)
                            configureDataOutputStream(s);
                        return s;
                    }
                };
            break;
        default:
            throw new IllegalArgumentException("connector");
        }

        rtpConnectorChanged(oldValue, rtpConnector);
    }

    /**
     * Sets the <tt>MediaDevice</tt> that this stream should use to play back
     * and capture media.
     * <p>
     * <b>Note</b>: Also resets any previous direction set with
     * {@link #setDirection(MediaDirection)} to the direction of the specified
     * <tt>MediaDevice</tt>.
     * </p>
     *
     * @param device the <tt>MediaDevice</tt> that this stream should use to
     * play back and capture media
     * @see MediaStream#setDevice(MediaDevice)
     */
    public void setDevice(MediaDevice device)
    {
        if (device == null)
            throw new NullPointerException("device");

        // Require AbstractMediaDevice for MediaDeviceSession support.
        AbstractMediaDevice abstractMediaDevice = (AbstractMediaDevice) device;

        if ((deviceSession == null) || (deviceSession.getDevice() != device))
        {
            assertDirection(direction, device.getDirection(), "device");

            MediaDeviceSession oldValue = deviceSession;
            MediaFormat format;
            MediaDirection startedDirection;
            if (deviceSession != null)
            {
                format = getFormat();
                startedDirection = deviceSession.getStartedDirection();

                deviceSession.removePropertyChangeListener(
                        deviceSessionPropertyChangeListener);

                // keep player active
                deviceSession.setDisposePlayerOnClose(
                        !(deviceSession instanceof VideoMediaDeviceSession));
                deviceSession.close();
                deviceSession = null;
            }
            else
            {
                format = null;
                startedDirection = MediaDirection.INACTIVE;
            }

            deviceSession = abstractMediaDevice.createSession();

            /*
             * Copy the playback from the old MediaDeviceSession into the new
             * MediaDeviceSession in order to prevent the recreation of the
             * playback of the ReceiveStream(s) when just changing the
             * MediaDevice of this MediaSteam.
             */
            if (oldValue != null)
                deviceSession.copyPlayback(oldValue);

            deviceSession.addPropertyChangeListener(
                    deviceSessionPropertyChangeListener);

            /*
             * Setting a new device resets any previously-set direction.
             * Otherwise, we risk not being able to set a new device if it is
             * mandatory for the new device to fully cover any previously-set
             * direction.
             */
            direction = null;

            if (deviceSession != null)
            {
                if (format != null)
                    deviceSession.setFormat(format);
                deviceSession.setMute(mute);
            }
            deviceSessionChanged(oldValue, deviceSession);
            if (deviceSession != null)
            {
                deviceSession.start(startedDirection);
                synchronized (receiveStreams)
                {
                    for (ReceiveStream receiveStream : receiveStreams)
                        deviceSession.addReceiveStream(receiveStream);
                }
            }
        }
    }

    /**
     * Sets the direction in which media in this <tt>MediaStream</tt> is to be
     * streamed. If this <tt>MediaStream</tt> is not currently started, calls to
     * {@link #start()} later on will start it only in the specified
     * <tt>direction</tt>. If it is currently started in a direction different
     * than the specified, directions other than the specified will be stopped.
     *
     * @param direction the <tt>MediaDirection</tt> in which this
     * <tt>MediaStream</tt> is to stream media when it is started
     * @see MediaStream#setDirection(MediaDirection)
     */
    public void setDirection(MediaDirection direction)
    {
        if (direction == null)
            throw new NullPointerException("direction");
        if(this.direction == direction)
            return;

        if(logger.isTraceEnabled())
        {
            logger.trace(
                    "Changing direction of stream " + hashCode()
                        + " from:" + this.direction
                        + " to:" + direction);
        }

        /*
         * Make sure that the specified direction is in accord with the
         * direction of the MediaDevice of this instance.
         */
        assertDirection(direction, getDeviceDirection(), "direction");

        this.direction = direction;

        switch (this.direction)
        {
        case INACTIVE:
            stop(MediaDirection.SENDRECV);
            return;
        case RECVONLY:
            stop(MediaDirection.SENDONLY);
            break;
        case SENDONLY:
            stop(MediaDirection.RECVONLY);
            break;
        case SENDRECV:
            break;
        default:
            // Don't know what it may be (in the future) so ignore it.
            return;
        }
        if (started)
            start(this.direction);
    }

    /**
     * Sets the <tt>MediaFormat</tt> that this <tt>MediaStream</tt> should
     * transmit in.
     *
     * @param format the <tt>MediaFormat</tt> that this <tt>MediaStream</tt>
     * should transmit in
     * @see MediaStream#setFormat(MediaFormat)
     */
    public void setFormat(MediaFormat format)
    {
        MediaDeviceSession deviceSession = getDeviceSession();
        MediaFormatImpl<? extends Format> deviceSessionFormat = null;

        if (deviceSession != null)
        {
            deviceSessionFormat = deviceSession.getFormat();
            if ((deviceSessionFormat != null)
                    && deviceSessionFormat.equals(format)
                    && deviceSessionFormat.advancedAttributesAreEqual(
                            deviceSessionFormat.getAdvancedAttributes(),
                            format.getAdvancedAttributes()))
            {
                return;
            }
        }

        if (logger.isTraceEnabled())
        {
            logger.trace(
                    "Changing format of stream " + hashCode()
                        + " from: " + deviceSessionFormat
                        + " to: " + format);
        }

        handleAttributes(format, format.getAdvancedAttributes());
        handleAttributes(format, format.getFormatParameters());
        deviceSession.setFormat(format);
    }

    /**
     * Handles attributes contained in <tt>MediaFormat</tt>.
     *
     * @param format the <tt>MediaFormat</tt> to handle the attributes of
     * @param attrs the attributes <tt>Map</tt> to handle
     */
    protected void handleAttributes(
            MediaFormat format,
            Map<String, String> attrs)
    {
    }

    /**
     * Causes this <tt>MediaStream</tt> to stop transmitting the media being fed
     * from this stream's <tt>MediaDevice</tt> and transmit "silence" instead.
     * "Silence" for video is understood as video data which is not the captured
     * video data and may represent, for example, a black image.
     *
     * @param mute <tt>true</tt> to have this <tt>MediaStream</tt> transmit
     * "silence" instead of the actual media data that it captures from its
     * <tt>MediaDevice</tt>; <tt>false</tt> to transmit actual media data
     * captured from the <tt>MediaDevice</tt> of this <tt>MediaStream</tt>
     * @see MediaStream#setMute(boolean)
     */
    public void setMute(boolean mute)
    {
        if (this.mute != mute)
        {
            if(logger.isTraceEnabled())
                logger.trace((mute? "Muting" : "Unmuting")
                        + " stream with hashcode " + hashCode());

            this.mute = mute;

            MediaDeviceSession deviceSession = getDeviceSession();

            if (deviceSession != null)
                deviceSession.setMute(this.mute);
        }
    }

    /**
     * Returns 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).
     *
     * @return 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 MediaStream#setTarget(MediaStreamTarget)
     */
    public MediaStreamTarget getTarget()
    {
        return rtpConnectorTarget;
    }

    /**
     * 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).
     *
     * @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 MediaStream#setTarget(MediaStreamTarget)
     */
    public void setTarget(MediaStreamTarget target)
    {
        // Short-circuit if setting the same target.
        if (target == null)
        {
            if (rtpConnectorTarget == null)
                return;
        }
        else if (target.equals(rtpConnectorTarget))
            return;

        doSetTarget(target);
    }

    /**
     * Starts capturing media from this stream's <tt>MediaDevice</tt> and then
     * streaming it through the local <tt>StreamConnector</tt> toward the
     * stream's target address and port. Also puts the <tt>MediaStream</tt> in a
     * listening state which make it play all media received from the
     * <tt>StreamConnector</tt> on the stream's <tt>MediaDevice</tt>.
     *
     * @see MediaStream#start()
     */
    public void start()
    {
        start(getDirection());
        started = true;
    }

    /**
     * Starts the processing of media in this instance in a specific direction.
     *
     * @param direction a <tt>MediaDirection</tt> value which represents the
     * direction of the processing of media to be started. For example,
     * {@link MediaDirection#SENDRECV} to start both capture and playback of
     * media in this instance or {@link MediaDirection#SENDONLY} to only start
     * the capture of media in this instance
     */
    private void start(MediaDirection direction)
    {
        if (direction == null)
            throw new NullPointerException("direction");

        /*
         * If the local peer is the focus of a conference for which it is to
         * perform RTP translation even without generating media to be sent, it
         * should create its StreamRTPManager.
         */
        boolean getRTPManagerForRTPTranslator = true;

        if (direction.allowsSending()
                && ((startedDirection == null)
                        || !startedDirection.allowsSending()))
        {
            /*
             * The startSendStreams method will be called so the getRTPManager
             * method will be called as part of the execution of the former.
             */
            getRTPManagerForRTPTranslator = false;
            startSendStreams();

            getDeviceSession().start(MediaDirection.SENDONLY);

            if (MediaDirection.RECVONLY.equals(startedDirection))
                startedDirection = MediaDirection.SENDRECV;
            else if (startedDirection == null)
                startedDirection = MediaDirection.SENDONLY;

            if (logger.isInfoEnabled())
            {
                MediaType mediaType = getMediaType();
                MediaStreamStats stats = getMediaStreamStats();

                logger.info(
                        mediaType
                            + " codec/freq: "
                            + stats.getEncoding()
                            + "/"
                            + stats.getEncodingClockRate()
                            + " Hz");
                logger.info(
                        mediaType
                            + " remote IP/port: "
                            + stats.getRemoteIPAddress()
                            + "/"
                            + String.valueOf(stats.getRemotePort()));
            }
        }

        if (direction.allowsReceiving()
                && ((startedDirection == null)
                        || !startedDirection.allowsReceiving()))
        {
            /*
             * The startReceiveStreams method will be called so the
             * getRTPManager method will be called as part of the execution of
             * the former.
             */
            getRTPManagerForRTPTranslator = false;

            startReceiveStreams();

            getDeviceSession().start(MediaDirection.RECVONLY);

            if (MediaDirection.SENDONLY.equals(startedDirection))
                startedDirection = MediaDirection.SENDRECV;
            else if (startedDirection == null)
                startedDirection = MediaDirection.RECVONLY;
        }

        /*
         * If the local peer is the focus of a conference for which it is to
         * perform RTP translation even without generating media to be sent, it
         * should create its StreamRTPManager.
         */
        if (getRTPManagerForRTPTranslator && (rtpTranslator != null))
            getRTPManager();
    }

    /**
     * Starts the <tt>ReceiveStream</tt>s that this instance is receiving from
     * its remote peer. By design, a <tt>MediaStream</tt> instance is associated
     * with a single <tt>ReceiveStream</tt> at a time. However, the
     * <tt>ReceiveStream</tt>s are created by <tt>RTPManager</tt> and it tracks
     * multiple <tt>ReceiveStream</tt>s. In practice, the <tt>RTPManager</tt> of
     * this <tt>MediaStreamImpl</tt> will have a single <tt>ReceiveStream</tt>
     * in its list.
     */
    @SuppressWarnings("unchecked")
    private void startReceiveStreams()
    {
        StreamRTPManager rtpManager = getRTPManager();
        List<ReceiveStream> receiveStreams;

        try
        {
            receiveStreams = rtpManager.getReceiveStreams();
        }
        catch (Exception ex)
        {
            /*
             * It appears that in early call states when there are no streams, a
             * NullPointerException could be thrown. Make sure we handle it
             * gracefully.
             */
            if (logger.isTraceEnabled())
                logger.trace("Failed to retrieve receive streams", ex);
            receiveStreams = null;
        }

        if (receiveStreams != null)
        {
            /*
             * It turns out that the receiveStreams list of rtpManager can be
             * empty. As a workaround, use the receiveStreams of this instance.
             */
            if (receiveStreams.isEmpty() && (this.receiveStreams != null))
                receiveStreams = this.receiveStreams;

            for (ReceiveStream receiveStream : receiveStreams)
            {
                try
                {
                    DataSource receiveStreamDataSource
                        = receiveStream.getDataSource();

                    /*
                     * For an unknown reason, the stream DataSource can be null
                     * at the end of the Call after re-INVITEs have been
                     * handled.
                     */
                    if (receiveStreamDataSource != null)
                        receiveStreamDataSource.start();
                }
                catch (IOException ioex)
                {
                    logger.warn(
                            "Failed to start receive stream " + receiveStream,
                            ioex);
                }
            }
        }
    }

    /**
     * Starts the <tt>SendStream</tt>s of the <tt>RTPManager</tt> of this
     * <tt>MediaStreamImpl</tt>.
     */
    private void startSendStreams()
    {
        /*
         * Until it's clear that the SendStreams are required (i.e. we've
         * negotiated to send), they will not be created. Otherwise, their
         * creation isn't only illogical but also causes the CaptureDevice to
         * be used.
         */
        if (!sendStreamsAreCreated)
            createSendStreams();
        StreamRTPManager rtpManager = getRTPManager();
        @SuppressWarnings("unchecked")
        Iterable<SendStream> sendStreams = rtpManager.getSendStreams();

        if (sendStreams != null)
        {
            for (SendStream sendStream : sendStreams)
            {
                try
                {
                    DataSource sendStreamDataSource
                        = sendStream.getDataSource();

                    // TODO Are we sure we want to connect here?
                    sendStreamDataSource.connect();
                    sendStream.start();
                    sendStreamDataSource.start();

                    if (logger.isTraceEnabled())
                    {
                        logger.trace(
                                "Started SendStream with hashCode "
                                    + sendStream.hashCode());
                    }
                }
                catch (IOException ioe)
                {
                    logger.warn("Failed to start stream " + sendStream, ioe);
                }
            }
        }
    }

    /**
     * Stops all streaming and capturing in this <tt>MediaStream</tt> and closes
     * and releases all open/allocated devices/resources. Has no effect if this
     * <tt>MediaStream</tt> is already closed and is simply ignored.
     *
     * @see MediaStream#stop()
     */
    public void stop()
    {
        stop(MediaDirection.SENDRECV);
        started = false;
    }

    /**
     * Stops the processing of media in this instance in a specific direction.
     *
     * @param direction a <tt>MediaDirection</tt> value which represents the
     * direction of the processing of media to be stopped. For example,
     * {@link MediaDirection#SENDRECV} to stop both capture and playback of
     * media in this instance or {@link MediaDirection#SENDONLY} to only stop
     * the capture of media in this instance
     */
    private void stop(MediaDirection direction)
    {
        if (direction == null)
            throw new NullPointerException("direction");

        if (rtpManager == null)
            return;

        if ((MediaDirection.SENDRECV.equals(direction)
                    || MediaDirection.SENDONLY.equals(direction))
                && (MediaDirection.SENDRECV.equals(startedDirection)
                        || MediaDirection.SENDONLY.equals(startedDirection)))
        {
            /*
             * XXX It is not very clear at the time of this writing whether the
             * SendStreams are to be stopped or closed. On one hand, stopping a
             * direction may be a temporary transition which relies on retaining
             * the SSRC. On the other hand, it may be permanent. In which case,
             * the respective ReveiveStream on the remote peer will timeout at
             * some point in time. In the context of video conferences, when a
             * member stops the streaming of their video without leaving the
             * conference, they will stop their SendStreams. However, the other
             * members will need respective BYE RTCP packets in order to know
             * that they are to remove the associated ReceiveStreams from
             * display. The initial version of the code here used to stop the
             * SendStreams without closing them but, given the considerations
             * above, it is being changed to close them in the case of video.
             */
            stopSendStreams(this instanceof VideoMediaStream);

            if (deviceSession != null)
                deviceSession.stop(MediaDirection.SENDONLY);

            if (MediaDirection.SENDRECV.equals(startedDirection))
                startedDirection = MediaDirection.RECVONLY;
            else if (MediaDirection.SENDONLY.equals(startedDirection))
                startedDirection = null;
        }

        if ((MediaDirection.SENDRECV.equals(direction)
                || MediaDirection.RECVONLY.equals(direction))
            && (MediaDirection.SENDRECV.equals(startedDirection)
                    || MediaDirection.RECVONLY.equals(startedDirection)))
        {
            stopReceiveStreams();

            if (deviceSession != null)
                deviceSession.stop(MediaDirection.RECVONLY);

            if (MediaDirection.SENDRECV.equals(startedDirection))
                startedDirection = MediaDirection.SENDONLY;
            else if (MediaDirection.RECVONLY.equals(startedDirection))
                startedDirection = null;
        }
    }

    /**
     * Stops the <tt>ReceiveStream</tt>s that this instance is receiving from
     * its remote peer. By design, a <tt>MediaStream</tt> instance is associated
     * with a single <tt>ReceiveStream</tt> at a time. However, the
     * <tt>ReceiveStream</tt>s are created by <tt>RTPManager</tt> and it tracks
     * multiple <tt>ReceiveStream</tt>s. In practice, the <tt>RTPManager</tt> of
     * this <tt>MediaStreamImpl</tt> will have a single <tt>ReceiveStream</tt>
     * in its list.
     */
    @SuppressWarnings("unchecked")
    private void stopReceiveStreams()
    {
        List<ReceiveStream> receiveStreams;

        try
        {
            receiveStreams = rtpManager.getReceiveStreams();
        }
        catch (Exception ex)
        {
            /*
             * It appears that in early call states when there are no streams, a
             * NullPointerException could be thrown. Make sure we handle it
             * gracefully.
             */
            if (logger.isTraceEnabled())
                logger.trace("Failed to retrieve receive streams", ex);
            receiveStreams = null;
        }

        if (receiveStreams != null)
        {
            /*
             * It turns out that the receiveStreams list of rtpManager can be
             * empty. As a workaround, use the receiveStreams of this instance.
             */
            if (receiveStreams.isEmpty() && (this.receiveStreams != null))
                receiveStreams = this.receiveStreams;

            for (ReceiveStream receiveStream : receiveStreams)
            {
                try
                {
                    if (logger.isTraceEnabled())
                    {
                        logger.trace(
                                "Stopping receive stream with hashcode "
                                    + receiveStream.hashCode());
                    }

                    DataSource receiveStreamDataSource
                        = receiveStream.getDataSource();

                    /*
                     * For an unknown reason, the stream DataSource can be null
                     * at the end of the Call after re-INVITEs have been
                     * handled.
                     */
                    if (receiveStreamDataSource != null)
                        receiveStreamDataSource.stop();
                }
                catch (IOException ioex)
                {
                    logger.warn(
                            "Failed to stop receive stream " + receiveStream,
                            ioex);
                }
            }
        }
    }

    /**
     * Stops the <tt>SendStream</tt>s that this instance is sending to its
     * remote peer and optionally closes them.
     *
     * @param close <tt>true</tt> to close the <tt>SendStream</tt>s that this
     * instance is sending to its remote peer after stopping them;
     * <tt>false</tt> to only stop them
     * @return the <tt>SendStream</tt>s which were stopped
     */
    private Iterable<SendStream> stopSendStreams(boolean close)
    {
        if (rtpManager == null)
            return null;

        @SuppressWarnings("unchecked")
        Iterable<SendStream> sendStreams = rtpManager.getSendStreams();
        Iterable<SendStream> stoppedSendStreams
            = stopSendStreams(sendStreams, close);

        if (close)
            sendStreamsAreCreated = false;

        return stoppedSendStreams;
    }

    /**
     * Stops specific <tt>SendStream</tt>s and optionally closes them.
     *
     * @param sendStreams the <tt>SendStream</tt>s to be stopped and optionally
     * closed
     * @param close <tt>true</tt> to close the specified <tt>SendStream</tt>s
     * after stopping them; <tt>false</tt> to only stop them
     * @return the stopped <tt>SendStream</tt>s
     */
    private Iterable<SendStream> stopSendStreams(
            Iterable<SendStream> sendStreams,
            boolean close)
    {
        if (sendStreams == null)
            return null;

        for (SendStream sendStream : sendStreams)
        {
            try
            {
                if (logger.isTraceEnabled())
                {
                    logger.trace(
                            "Stopping send stream with hashcode "
                                + sendStream.hashCode());
                }

                sendStream.getDataSource().stop();
                sendStream.stop();

                if (close)
                {
                    try
                    {
                        sendStream.close();
                    }
                    catch (NullPointerException npe)
                    {
                        /*
                         * Sometimes com.sun.media.rtp.RTCPTransmitter#bye() may
                         * throw NullPointerException but it does not seem to be
                         * guaranteed because it does not happen while debugging
                         * and stopping at a breakpoint on SendStream#close().
                         * One of the cases in which it appears upon call
                         * hang-up is if we do not close the "old" SendStreams
                         * upon reinvite(s). Though we are now closing such
                         * SendStreams, ignore the exception here just in case
                         * because we already ignore IOExceptions.
                         */
                        logger.error(
                                "Failed to close send stream " + sendStream,
                                npe);
                    }
                }
            }
            catch (IOException ioe)
            {
                logger.warn("Failed to stop send stream " + sendStream, ioe);
            }
        }
        return sendStreams;
    }

    /**
     * Returns a human-readable representation of a specific <tt>DataSource</tt>
     * instance in the form of a <tt>String</tt> value.
     *
     * @param dataSource the <tt>DataSource</tt> to return a human-readable
     * representation of
     * @return a <tt>String</tt> value which gives a human-readable
     * representation of the specified <tt>dataSource</tt>
     */
    public static String toString(DataSource dataSource)
    {
        StringBuffer str = new StringBuffer();

        str.append(dataSource.getClass().getSimpleName());
        str.append(" with hashCode ");
        str.append(dataSource.hashCode());

        MediaLocator locator = dataSource.getLocator();

        if (locator != null)
        {
            str.append(" and locator ");
            str.append(locator);
        }
        return str.toString();
    }

    /**
     * Notifies this <tt>ReceiveStreamListener</tt> that the <tt>RTPManager</tt>
     * it is registered with has generated an event related to a <tt>ReceiveStream</tt>.
     *
     * @param event the <tt>ReceiveStreamEvent</tt> which specifies the
     * <tt>ReceiveStream</tt> that is the cause of the event and the very type
     * of the event
     * @see ReceiveStreamListener#update(ReceiveStreamEvent)
     */
    public void update(ReceiveStreamEvent event)
    {
        if (event instanceof NewReceiveStreamEvent)
        {
            ReceiveStream receiveStream = event.getReceiveStream();

            if (receiveStream != null)
            {
                long receiveStreamSSRC = receiveStream.getSSRC();

                if (logger.isTraceEnabled())
                {
                    logger.trace(
                            "Received new ReceiveStream with ssrc "
                                + receiveStreamSSRC);
                }

                addRemoteSourceID(receiveStreamSSRC);

                synchronized (receiveStreams)
                {
                    if (!receiveStreams.contains(receiveStream))
                    {
                        receiveStreams.add(receiveStream);

                        MediaDeviceSession deviceSession = getDeviceSession();

                        if (deviceSession != null)
                            deviceSession.addReceiveStream(receiveStream);
                    }
                }
            }
        }
        else if (event instanceof TimeoutEvent)
        {
            ReceiveStream receiveStream = event.getReceiveStream();

            /*
             * If we recreate streams, we will already have restarted
             * zrtpControl. But when on the other end someone recreates his
             * streams, we will receive a ByeEvent (which extends TimeoutEvent)
             * and then we must also restart our ZRTP. This happens, for
             * example, when we are already in a call and the remote peer
             * converts his side of the call into a conference call.
             */
            /*
            if(!zrtpRestarted)
                restartZrtpControl();
            */

            if (receiveStream != null)
            {
                synchronized (receiveStreams)
                {
                    if (receiveStreams.contains(receiveStream))
                    {
                        receiveStreams.remove(receiveStream);

                        MediaDeviceSession deviceSession = getDeviceSession();

                        if (deviceSession != null)
                            deviceSession.removeReceiveStream(receiveStream);
                    }
                }
            }
        }
    }

    /**
     * Notifies this <tt>SendStreamListener</tt> that the <tt>RTPManager</tt> it
     * is registered with has generated an event related to a <tt>SendStream</tt>.
     *
     * @param event the <tt>SendStreamEvent</tt> which specifies the
     * <tt>SendStream</tt> that is the cause of the event and the very type of
     * the event
     * @see SendStreamListener#update(SendStreamEvent)
     */
    public void update(SendStreamEvent event)
    {
        if (event instanceof NewSendStreamEvent)
        {
            long localSourceID = event.getSendStream().getSSRC();

            if (getLocalSourceID() != localSourceID)
                setLocalSourceID(localSourceID);
        }
    }

    /**
     * Notifies this <tt>SessionListener</tt> that the <tt>RTPManager</tt> it is
     * registered with has generated an event which pertains to the session as a
     * whole and does not belong to a <tt>ReceiveStream</tt> or a
     * <tt>SendStream</tt> or a remote participant necessarily.
     *
     * @param event the <tt>SessionEvent</tt> which specifies the source and the
     * very type of the event
     * @see SessionListener#update(SessionEvent)
     */
    public void update(SessionEvent event)
    {
        // TODO Auto-generated method stub
    }

    /**
     * Method called back in the RemoteListener to notify
     * listener of all RTP Remote Events.RemoteEvents are one of
     * ReceiverReportEvent, SenderReportEvent or RemoteCollisionEvent
     *
     * @param remoteEvent the event
     */
    public void update(RemoteEvent remoteEvent)
    {
        if(!logger.isInfoEnabled())
            return;

        if(remoteEvent instanceof SenderReportEvent)
        {
            numberOfReceivedSenderReports++;

            SenderReport report =
                    ((SenderReportEvent)remoteEvent).getReport();

            Feedback feedback = null;
            long remoteJitter = -1;

            if(report.getFeedbackReports().size() > 0)
            {
                feedback = (Feedback)report.getFeedbackReports().get(0);

                remoteJitter = feedback.getJitter();

                if((remoteJitter < minRemoteInterArrivalJitter)
                        || (minRemoteInterArrivalJitter == -1))
                    minRemoteInterArrivalJitter = remoteJitter;

                if(maxRemoteInterArrivalJitter < remoteJitter)
                    maxRemoteInterArrivalJitter = remoteJitter;
            }

            // As sender reports are received on every 5 seconds
            // print every 4th packet, on every 20 seconds
            if(numberOfReceivedSenderReports%4 != 1)
                return;

            StringBuilder buff
                = new StringBuilder(StatisticsEngine.RTP_STAT_PREFIX);
            MediaType mediaType = getMediaType();
            String mediaTypeStr
                = (mediaType == null) ? "" : mediaType.toString();

            buff.append("Received a report for ")
                .append(mediaTypeStr)
                .append(" stream SSRC:")
                .append(getLocalSourceID())
                .append(" [packet count:")
                .append(report.getSenderPacketCount())
                .append(", bytes:").append(report.getSenderByteCount());

                if(feedback != null)
                {
                    buff.append(", interarrival jitter:")
                            .append(remoteJitter)
                    .append(", lost packets:").append(feedback.getNumLost())
                    .append(", time since previous report:")
                            .append((int) (feedback.getDLSR() / 65.536))
                            .append("ms");
                }
            buff.append(" ]");
            logger.info(buff);
        }
    }

    /**
     * Sets the local SSRC identifier and fires the corresponding
     * <tt>PropertyChangeEvent</tt>.
     *
     * @param localSourceID the SSRC identifier that this stream will be using
     * in outgoing RTP packets from now on
     */
    protected void setLocalSourceID(long localSourceID)
    {
        if (this.localSourceID != localSourceID)
        {
            Long oldValue = this.localSourceID;

            this.localSourceID = localSourceID;

            /*
             * If a ZRTP engine is available, then let it know about the
             * SSRC of the new SendStream. Currently, ZRTP supports only one
             * SSRC per engine.
             */
            TransformEngine engine = srtpControl.getTransformEngine();
            if ((engine != null) && (engine instanceof ZRTPTransformEngine))
            {
                ((ZRTPTransformEngine)engine).setOwnSSRC(getLocalSourceID());
            }

            firePropertyChange(PNAME_LOCAL_SSRC, oldValue, this.localSourceID);
        }
    }

    /**
     * Sets the remote SSRC identifier and fires the corresponding
     * <tt>PropertyChangeEvent</tt>.
     *
     * @param remoteSourceID the SSRC identifier that this stream will be using
     * in outgoing RTP packets from now on.
     */
    protected void addRemoteSourceID(long remoteSourceID)
    {
        Long oldValue = getRemoteSourceID();

        if(!remoteSourceIDs.contains(remoteSourceID))
            remoteSourceIDs.add(remoteSourceID);

        firePropertyChange(PNAME_REMOTE_SSRC, oldValue, remoteSourceID);
    }

    /**
     * Returns the list of CSRC identifiers for all parties currently known
     * to contribute to the media that this stream is sending toward its remote
     * counter part. In other words, the method returns the list of CSRC IDs
     * that this stream will include in outgoing RTP packets. This method will
     * return an <tt>null</tt> in case this stream is not part of a mixed
     * conference call.
     *
     * @return a <tt>long[]</tt> array of CSRC IDs representing parties that are
     * currently known to contribute to the media that this stream is sending
     * or an <tt>null</tt> in case this <tt>MediaStream</tt> is not part of a
     * conference call.
     */
    public long[] getLocalContributingSourceIDs()
    {
        return localContributingSourceIDList;
    }

    /**
     * Returns the <tt>List</tt> of CSRC identifiers representing the parties
     * contributing to the stream that we are receiving from this
     * <tt>MediaStream</tt>'s remote party.
     *
     * @return a <tt>List</tt> of CSRC identifiers representing the parties
     * contributing to the stream that we are receiving from this
     * <tt>MediaStream</tt>'s remote party.
     */
    public long[] getRemoteContributingSourceIDs()
    {
        long[] remoteSsrcList = getDeviceSession().getRemoteSSRCList();

        // TODO implement

        return remoteSsrcList;
    }

    /**
     * Used to set the priority of the receive/send streams. Underling
     * implementations can override this and return different than
     * current default value.
     *
     * @return the priority for the current thread.
     */
    protected int getPriority()
    {
        return Thread.currentThread().getPriority();
    }

    /**
     * Prints all statistics available for {@link #rtpManager}.
     *
     * @param rtpManager the <tt>RTPManager</tt> to print statistics for
     */
    private void printFlowStatistics(StreamRTPManager rtpManager)
    {
        try
        {
            if(!logger.isInfoEnabled())
                return;

            //print flow statistics.
            GlobalTransmissionStats s = rtpManager.getGlobalTransmissionStats();

            StringBuilder buff
                = new StringBuilder(StatisticsEngine.RTP_STAT_PREFIX);
            MediaType mediaType = getMediaType();
            String mediaTypeStr
                = (mediaType == null) ? "" : mediaType.toString();

            buff.append("call stats for outgoing ")
                .append(mediaTypeStr)
                .append(" stream SSRC:")
                .append(getLocalSourceID())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("bytes sent: ").append(s.getBytesSent())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("RTP sent: ").append(s.getRTPSent())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("remote reported min interarrival jitter : ")
                        .append(minRemoteInterArrivalJitter)
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("remote reported max interarrival jitter : ")
                        .append(maxRemoteInterArrivalJitter)
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("local collisions: ").append(s.getLocalColls())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("remote collisions: ").append(s.getRemoteColls())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("RTCP sent: ").append(s.getRTCPSent())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("transmit failed: ").append(s.getTransmitFailed());

            logger.info(buff);

            GlobalReceptionStats rs = rtpManager.getGlobalReceptionStats();
            MediaFormat format = getFormat();

            buff = new StringBuilder(StatisticsEngine.RTP_STAT_PREFIX);
            buff.append("call stats for incoming ")
                .append((format == null) ? "" : format)
                .append(" stream SSRC:")
                .append(getRemoteSourceID())
                .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("packets received: ").append(rs.getPacketsRecd())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("bytes received: ").append(rs.getBytesRecd())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("packets lost: ").append(statisticsEngine.getLost())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("min interarrival jitter : ")
                    .append(statisticsEngine.getMinInterArrivalJitter())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("max interarrival jitter : ")
                    .append(statisticsEngine.getMaxInterArrivalJitter())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("RTCPs received: ").append(rs.getRTCPRecd())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("bad RTCP packets: ").append(rs.getBadRTCPPkts())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("bad RTP packets: ").append(rs.getBadRTPkts())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("local collisions: ").append(rs.getLocalColls())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("malformed BYEs: ").append(rs.getMalformedBye())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("malformed RRs: ").append(rs.getMalformedRR())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("malformed SDESs: ").append(rs.getMalformedSDES())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("malformed SRs: ").append(rs.getMalformedSR())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("packets looped: ").append(rs.getPacketsLooped())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("remote collisions: ").append(rs.getRemoteColls())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("SRRs received: ").append(rs.getSRRecd())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("transmit failed: ").append(rs.getTransmitFailed())
                    .append("\n").append(StatisticsEngine.RTP_STAT_PREFIX)
                .append("unknown types: ").append(rs.getUnknownTypes());

            logger.info(buff);
        }
        catch(Throwable t)
        {
            logger.error("Error writing statistics", t);
        }
    }

    private void printReceiveStreamStatistics()
    {
        mediaStreamStatsImpl.updateStats();
        StringBuilder buff = new StringBuilder("\nReceive stream stats: " +
            "discarded RTP packets: ")
                .append(mediaStreamStatsImpl.getNbDiscarded())
                .append("\n").append("Receive stream stats: " +
                    "decoded with FEC: ")
                .append(mediaStreamStatsImpl.getNbFec());
        logger.info(buff);
    }

    /**
     * Sets the <tt>RTPTranslator</tt> which is to forward RTP and RTCP traffic
     * between this and other <tt>MediaStream</tt>s.
     *
     * @param rtpTranslator the <tt>RTPTranslator</tt> which is to forward RTP
     * and RTCP traffic between this and other <tt>MediaStream</tt>s
     */
    public void setRTPTranslator(RTPTranslator rtpTranslator)
    {
        if (this.rtpTranslator != rtpTranslator)
            this.rtpTranslator = rtpTranslator;
    }

    /**
     * Returns a MediaStreamStats object used to get statistics about this
     * MediaStream.
     *
     * @return the MediaStreamStats object used to compute the statistics about
     * this MediaStream.
     */
    public MediaStreamStats getMediaStreamStats()
    {
        return this.mediaStreamStatsImpl;
    }

    /**
     * Gets the <tt>MediaType</tt> of this <tt>MediaStream</tt>.
     *
     * @return the <tt>MediaType</tt> of this <tt>MediaStream</tt>
     */
    public MediaType getMediaType()
    {
        MediaFormat format = getFormat();
        MediaType mediaType = null;

        if (format != null)
            mediaType = format.getMediaType();
        if (mediaType == null)
        {
            MediaDeviceSession deviceSession = getDeviceSession();

            if (deviceSession != null)
                mediaType = deviceSession.getDevice().getMediaType();
            if (mediaType == null)
            {
                if (this instanceof AudioMediaStream)
                    mediaType = MediaType.AUDIO;
                else if (this instanceof VideoMediaStream)
                    mediaType = MediaType.VIDEO;
            }
        }

        return mediaType;
    }

    /**
     * Returns an instance of <tt>FECDecoderControl</tt> associated with
     * <tt>receiveStream</tt>, if one is found in the <tt>deviceSession</tt>
     * and <tt>null</tt> otherwise.
     *
     * @param receiveStream The <tt>ReceiveStream</tt> to to return an
     * associated <tt>FECDecoderControl</tt> instance for.
     *
     * @return an instance of <tt>FECDecoderControl</tt> associated with
     * <tt>receiveStream</tt>, if one is found in the <tt>deviceSession</tt>
     * and <tt>null</tt> otherwise.
     */
    public FECDecoderControl getFecDecoderControl(ReceiveStream receiveStream)
    {

        TranscodingDataSource transcodingDataSource
                    = deviceSession.getTranscodingDataSource(receiveStream);

        if(transcodingDataSource == null)
            return null;

        Processor processor = transcodingDataSource.getTranscodingProcessor();

        if(processor == null)
            return null;

        //return the first found instance
        for(TrackControl tc : processor.getTrackControls())
        {
            for(Object control : tc.getControls())
            {
                if(control instanceof FECDecoderControl)
                        {
                            return (FECDecoderControl) control;
                        }
            }
        }
        return null;
    }
}