All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.sf.fmj.media.rtp.RTPSourceStream Maven / Gradle / Ivy

There is a newer version: 1.0.2-jitsi
Show newest version
package net.sf.fmj.media.rtp;

import java.lang.ref.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;

import javax.media.*;
import javax.media.control.*;
import javax.media.format.*;
import javax.media.protocol.*;

import net.sf.fmj.media.*;
import net.sf.fmj.media.protocol.*;
import net.sf.fmj.media.protocol.rtp.DataSource;
import net.sf.fmj.media.rtp.util.*;

/**
 * Implements a PushBufferStream which represents a stream of RTP
 * packets being received by the local user/peer.
 *
 * @author Boris Grozev
 * @author Damian Minkov
 * @author Lyubomir Marinov
 */
public class RTPSourceStream
    extends BasicSourceStream
    implements PushBufferStream
{
    /**
     * The timeout in milliseconds to be used by the invocations of
     * {@link Object#wait(long)}.
     */
    private static final long WAIT_TIMEOUT = 100L;

    private BufferControlImpl bc;

    /**
     * The jitter buffer associated with this instance in terms of behaviour,
     * logic agnostic of the very storage-related details and the simplest of
     * RTP packet queuing specifics which are abstracted by {@link #q}.
     */
    private JitterBufferBehaviour behaviour;

    private boolean bufferWhenStopped = true;

    /**
     * The indicator which determines whether {@link #close()} has been invoked
     * without an intervening invocation of {@link #connect()}.
     */
    private boolean closed = false;

    /**
     * The indicator which determines whether {@link #close()} is executing.
     */
    private boolean closing = false;

    /**
     * The DataSource which has initialized and has this instance as
     * its sourceStream.
     */
    final DataSource datasource;

    private Format format;

    /**
     * The sequence number of the last Buffer added to this instance.
     */
    private long lastSeqRecv = Buffer.SEQUENCE_UNKNOWN;

    /**
     * Sequence number of the last Buffer read from this instance.
     */
    private long lastSeqSent = Buffer.SEQUENCE_UNKNOWN;

    /**
     * The RTP packet queue/jitter buffer which implements the storage of the
     * RTP packets added to and read from this RTPSourceStream.
     */
    final JitterBuffer q;

    /**
     * The Condition which is used for synchronization purposes instead
     * of synchronizing a block on {@link #q} because the latter is not flexible
     * enough for the thread complexity of JitterBuffer.
     */
    private final Condition qCondition;

    /**
     * The Lock which is used for synchronization purposes instead of
     * synchronizing a block on {@link #q} because the latter is not flexible
     * enough for the thread complexity of JitterBuffer.
     */
    private final Lock qLock;

    private boolean started = false;

    private final Object startSyncRoot = new Object();

    /**
     * The statistics related to the RTP packet queue/jitter buffer associated
     * with this RTPSourceStream. Implements {@link PacketQueueControl}
     * on behalf of this instance.
     */
    final JitterBufferStats stats;

    private Thread thread;

    /**
     * The (unique) reason for invoking
     * {@link BufferTransferHandler#transferData(PushBufferStream)} on
     * {@link #transferHandler}. Introduced in order to prevent busy waits when
     * there are enough packets to be read without blocking but no read is
     * actually performed.
     */
    private long transferDataReason;

    private BufferTransferHandler transferHandler;

    public RTPSourceStream(DataSource datasource)
    {
        datasource.setSourceStream(this);
        this.datasource = datasource;

        q = new JitterBuffer(4);

        qCondition = q.condition;
        qLock = q.lock;

        stats = new JitterBufferStats(this);

        // RTPSourceStream and its related classes assume that there is always a
        // JitterBufferBehaviour instance (in order to avoid null checks and for
        // the sake of simplicity). Make sure a default behaviour is initialized
        // until a specific Format is set on this instance.
        setBehaviour(null);
    }

    /**
     * Adds buffer to the queue.
     *
     * In case the queue is full: if buffer's sequence number comes
     * before the sequence numbers of the Buffers in the queue, nothing
     * is done. Otherwise, a packet is dropped using PktQue.dropPkt()
     *
     * @param buffer the buffer to add
     * @param flag unused
     * @param rtprawreceiver used to access the 'socket buffer'?
     */
    public void add(Buffer buffer, boolean flag, RTPRawReceiver rtprawreceiver)
    {
        if (!started && !bufferWhenStopped)
            return;

        long bufferSN = buffer.getSequenceNumber();

        // The access to lastSeqSent is synchronized because it is concurrently
        // modified by multiple threads. The access to started and
        // bufferWhenStopped above is usually synchronized on startReq so they
        // are left out to avoid synchronization on multiple monitors.
        qLock.lock();
        try
        {

        if (lastSeqRecv - bufferSN > 256L)
        {
            Log.info("Resetting queue, last seq added: " + lastSeqRecv +
                    ", current seq: " + bufferSN);
            reset();
            lastSeqRecv = bufferSN;
        }

        stats.updateMaxSizeReached();
        stats.updateSizePerPacket(buffer);
        if (!behaviour.preAdd(buffer, rtprawreceiver))
            return;

        stats.incrementNbAdd();
        lastSeqRecv = bufferSN;
        boolean almostFull = false;

        if (q.noMoreFree())
        {
            // The queue cannot accommodate the current packet so we have to
            // drop a packet.
            stats.incrementDiscardedFull();
            long l = q.getFirstSeq();
            if (l != Buffer.SEQUENCE_UNKNOWN && bufferSN < l)
            {
                // The current/received packet is the earliest. Drop it by
                // simply not adding it.
                return;
            }
            behaviour.dropPkt();
        }

        if (q.getFreeCount() <= 1)
            almostFull = true;
        Buffer qBuffer = q.getFree();
        boolean added = false;

        try
        {
            byte[] bufferData = (byte[]) buffer.getData();
            byte[] qBufferData = (byte[]) qBuffer.getData();
            if ((qBufferData == null)
                    || (qBufferData.length < bufferData.length))
            {
                qBufferData = new byte[bufferData.length];
            }

            System.arraycopy(
                    bufferData, buffer.getOffset(),
                    qBufferData, buffer.getOffset(),
                    buffer.getLength());
            qBuffer.copy(buffer);
            qBuffer.setData(qBufferData);
            if (almostFull) //with this packet added, the queue will be full
            {
                qBuffer.setFlags(
                        qBuffer.getFlags()
                            | Buffer.FLAG_BUF_OVERFLOWN
                            | Buffer.FLAG_NO_DROP);
            }
            else
            {
                qBuffer.setFlags(
                        qBuffer.getFlags() | Buffer.FLAG_NO_DROP);
            }

            q.addPkt(qBuffer);
            added = true;
        }
        finally
        {
            if (!added)
                q.returnFree(qBuffer);
        }

        // A packet was added to this PushBufferStream so transferData.
        ++transferDataReason;
        // Well, do not transferData as soon as possible if the read will block
        // but rather transferData as soon as the read will not block.
        if (!behaviour.willReadBlock())
            qCondition.signalAll();

        }
        finally
        {
            qLock.unlock();
        }
    }

    public void close()
    {
        synchronized (startSyncRoot)
        {
            if (closing)
            {
                return;
            }
            else
            {
                closing = true;
                thread = null;
            }
            startSyncRoot.notifyAll();
        }
        try
        {
            if (!closed)
            {
                closed = true;

                stats.printStats();
                stop();

                // A deadlock was observed in the implementation using
                // synchronized blocks on q and Object.notifyAll(). In order to
                // fix the deadlock, the implementation was changed to use Lock
                // and Condition instead. If the Lock is not free, it should not
                // matter much that no signaling on the Condition will be
                // performed because the waiting threads will time out anyway.
                if (qLock.tryLock())
                {
                    try
                    {
                        qCondition.signalAll();
                    }
                    finally
                    {
                        qLock.unlock();
                    }
                }

                if (bc != null)
                    bc.removeSourceStream(this);
            }
        }
        finally
        {
            synchronized (startSyncRoot)
            {
                closing = false;
                startSyncRoot.notifyAll();
            }
        }
    }

    public void connect()
    {
        synchronized (startSyncRoot)
        {
            waitWhileClosing();
            closed = false;
        }
    }

    /**
     * Gets the JitterBufferBehaviour which represents the behaviour
     * exhibited by/the logic of the jitter buffer/RTP packet queue associated
     * with this instance.
     *
     * @return the JitterBufferBehaviour which represents the behaviour
     * exhibited by/the logic of the jitter buffer/RTP packet queue associated
     * with this instance
     */
    JitterBufferBehaviour getBehaviour()
    {
        return behaviour;
    }

    /**
     * Gets the BufferControlImpl set on this instance.
     *
     * @return the BufferControlImpl set on this instance
     */
    BufferControlImpl getBufferControl()
    {
        return bc;
    }

    /**
     * {@inheritDoc}
     *
     * Adds support for {@link PacketQueueControl}.
     */
    @Override
    public Object getControl(String controlType)
    {
        return
            JitterBufferControl.class.getName().equals(controlType)
                ? stats
                : super.getControl(controlType);
    }

    /**
     * {@inheritDoc}
     *
     * Adds support for {@link PacketQueueControl}.
     */
    @Override
    public Object[] getControls()
    {
        Object[] superControls = super.getControls();
        Object[] thisControls = new Object[superControls.length + 1];

        System.arraycopy(
                superControls, 0,
                thisControls, 0,
                superControls.length);
        thisControls[superControls.length] = stats;
        return thisControls;
    }

    @Override
    public Format getFormat()
    {
        return format;
    }

    /**
     * Gets the (RTP) sequence number of the last Buffer read out of
     * this SourceStream.
     *
     * @return the (RTP) sequence number of the last Buffer read out of
     * this SourceStream
     */
    long getLastReadSequenceNumber()
    {
        return lastSeqSent;
    }

    public void prebuffer()
    {
    }

    /**
     * Pops an element off the queue and copies it to buffer. The data
     * and header arrays of buffer are reused.
     *
     * @param buffer The Buffer object to copy an element of the queue
     * to.
     */
    @Override
    public void read(Buffer buffer)
    {
        // The access to lastSeqSent is synchronized because it is concurrently
        // modified by multiple threads.
        qLock.lock();
        try
        {
            try
            {
                behaviour.read(buffer);

                if (!buffer.isDiscard())
                    lastSeqSent = buffer.getSequenceNumber();
            }
            finally
            {
                // If a packet was read, schedule a transferData as soon as
                // possible in case there are more packets to be read.
                if (!buffer.isDiscard())
                {
                    ++transferDataReason;
                    qCondition.signalAll();
                }
            }
        }
        finally
        {
            qLock.unlock();
        }
    }

    /**
     * Resets the queue, dropping all packets.
     */
    public void reset()
    {
        // The access to lastSeqSent is synchronized because it is concurrently
        // modified by multiple threads.
        qLock.lock();
        try
        {
            stats.incrementNbReset();
            resetQ();
            behaviour.reset();
            lastSeqSent = Buffer.SEQUENCE_UNKNOWN;
        }
        finally
        {
            qLock.unlock();
        }
    }

    /**
     * Empties the queue by dropping all packets.
     */
    public void resetQ()
    {
        Log.comment("Resetting the RTP packet queue");
        qLock.lock();
        try
        {
            while (q.fillNotEmpty())
            {
                behaviour.dropPkt();
                stats.incrementDiscardedReset();
            }
            // All packets which could be read were dropped so there is hardly
            // any reason to transferData (as soon as possible).
            qCondition.signalAll();
        }
        finally
        {
            qLock.unlock();
        }
    }

    /**
     * Runs in {@link #thread}.
     *
     * @param runnable the TransferDataRunnable which is running in
     * the current thread
     * @return true if the current thread is to continue invoking the
     * method; otherwise, false
     */
    private boolean runInThread(TransferDataRunnable runnable)
    {
        synchronized (startSyncRoot)
        {
            // Is this RTPSourceStream still utilizing the current thread?
            if (!Thread.currentThread().equals(thread) || closing || closed)
            {
                return false;
            }
            // Has this RTPSourceStream been started?
            if (!started)
            {
                try
                {
                    startSyncRoot.wait(WAIT_TIMEOUT);
                }
                catch (InterruptedException ie)
                {
                }
                return true;
            }
        }

        // This RTPSourceStream has been started and may or may not have been
        // stopped and/or closed afterwards.
        BufferTransferHandler transferHandler = null;

        qLock.lock();
        try
        {
            boolean wait;

            if (behaviour.willReadBlock())
            {
                // Obviously, do not transferData because the read will block.
                wait = true;
            }
            else if (runnable.transferDataReason == transferDataReason)
            {
                // There was an invocation of transferData for that particular
                // reason.
                wait = true;
            }
            else
            {
                transferHandler = this.transferHandler;
                if (transferHandler == null)
                {
                    // It is impossible to transferData because there is no
                    // object on which to invoke it.
                    wait = true;
                }
                else
                {
                    // There is going to be an invocation of transferData bellow
                    // and it is going to be for that particular reason.
                    wait = false;
                    runnable.transferDataReason = transferDataReason;
                }
            }
            if (wait)
            {
                try
                {
                    qCondition.await(WAIT_TIMEOUT, TimeUnit.MILLISECONDS);
                }
                catch (InterruptedException ie)
                {
                }
                return true;
            }
        }
        finally
        {
            qLock.unlock();
        }

        if (transferHandler != null)
            transferHandler.transferData(this);

        return true;
    }

    /**
     * Sets a JitterBufferBehaviour which represents the behaviour to
     * be exhibited by/the logic of the jitter buffer/RTP packet queue
     * associated with this instance.
     *
     * @param behaviour the JitterBufferBehaviour which represents the
     * behaviour to be exhibited by the jitter buffer/RTP packet queue
     * associated with this instance. If null, the implementation
     * defaults to BasicJitterBufferBehaviour.
     */
    private void setBehaviour(JitterBufferBehaviour behaviour)
    {
        // In order to avoid null checks, RTPSourceStream and its related
        // classes assume that there is always a JitterBufferBehaviour instance.
        // Default to BasicJitterBufferBehaviour.
        if (behaviour == null)
        {
            if (this.behaviour instanceof BasicJitterBufferBehaviour)
                return;
            else
                behaviour = new BasicJitterBufferBehaviour(this);
        }

        this.behaviour = behaviour;
    }

    public void setBufferControl(BufferControl buffercontrol)
    {
        bc = (BufferControlImpl) buffercontrol;
        updateBuffer(bc.getBufferLength());
        updateThreshold(bc.getMinimumThreshold());
    }

    public void setBufferListener(BufferListener bufferlistener)
    {
    }

    public void setBufferWhenStopped(boolean flag)
    {
        bufferWhenStopped = flag;
    }

    void setContentDescriptor(String s)
    {
        contentDescriptor = new ContentDescriptor(s);
    }

    protected void setFormat(Format format)
    {
        if (this.format != format)
        {
            this.format = format;

            // The jitter buffer/RTP packet queue associated with
            // RTPSourceStream behaves in accord with the Format of the media.
            JitterBufferBehaviour behaviour;

            if (this.format instanceof AudioFormat)
                behaviour = new AudioJitterBufferBehaviour(this);
            else if (this.format instanceof VideoFormat)
                behaviour = new VideoJitterBufferBehaviour(this);
            else
                behaviour = null;
            setBehaviour(behaviour);
        }
    }

    @Override
    public void setTransferHandler(BufferTransferHandler transferHandler)
    {
        this.transferHandler = transferHandler;
    }

    public void start()
    {
        Log.info("Starting RTPSourceStream.");
        synchronized (startSyncRoot)
        {
            started = true;
            startThread();
            startSyncRoot.notifyAll();
        }

        // A deadlock was observed in the implementation using synchronized
        // blocks on q and Object.notifyAll(). In order to fix the deadlock, the
        // implementation was changed to use Lock and Condition instead. If the
        // Lock is not free, it should not matter much that no signaling on the
        // Condition will be performed because the waiting threads will time out
        // anyway.
        if (qLock.tryLock())
        {
            try
            {
                qCondition.signalAll();
            }
            finally
            {
                qLock.unlock();
            }
        }
    }

    /**
     * Initializes and starts {@link #thread} if it has not been initialized and
     * started yet.
     */
    private void startThread()
    {
        synchronized (startSyncRoot)
        {
            waitWhileClosing();
            if ((this.thread == null) && !closed)
            {
                RTPMediaThread thread
                    = new RTPMediaThread(
                            new TransferDataRunnable(this),
                            RTPSourceStream.class.getName());

                thread.setDaemon(true);
                thread.useControlPriority();

                boolean started = false;

                this.thread = thread;
                try
                {
                    thread.start();
                    started = true;
                }
                finally
                {
                    if (!started && thread.equals(this.thread))
                        this.thread = null;
                }
            }

            startSyncRoot.notifyAll();
        }
    }

    public void stop()
    {
        Log.info("Stopping RTPSourceStream.");
        synchronized (startSyncRoot)
        {
            started = false;
            startSyncRoot.notifyAll();
            if (!bufferWhenStopped)
                reset();
        }

        // A deadlock was observed in the implementation using synchronized
        // blocks on q and Object.notifyAll(). In order to fix the deadlock, the
        // implementation was changed to use Lock and Condition instead. If the
        // Lock is not free, it should not matter much that no signaling on the
        // Condition will be performed because the waiting threads will time out
        // anyway.
        if (qLock.tryLock())
        {
            try
            {
                qCondition.signalAll();
            }
            finally
            {
                qLock.unlock();
            }
        }
    }

    /**
     * Notifies this RTPSourceStream that its {@link #thread} may have
     * exited.
     *
     * @param runnable the TransferDataRunnable which has exited
     */
    private void threadExited(TransferDataRunnable runnable)
    {
        // The current thread cannot be utilized by this RTPSourceStream any
        // longer.
        synchronized (startSyncRoot)
        {
            if (Thread.currentThread().equals(thread))
            {
                thread = null;
                startSyncRoot.notifyAll();
            }
        }
    }

    public long updateBuffer(long l)
    {
        return l;
    }

    public long updateThreshold(long l)
    {
        return l;
    }

    /**
     * Wait on {@link #startSyncRoot} while {@link #closing} equals
     * true i.e. wait on startSyncRoot until false is
     * set on closing.
     */
    private void waitWhileClosing()
    {
        boolean interrupted = false;

        while (closing)
        {
            try
            {
                startSyncRoot.wait();
            }
            catch (InterruptedException ie)
            {
                interrupted = true;
            }
        }
        if (interrupted)
            Thread.currentThread().interrupt();
    }

    /**
     * Implements Runnable which is to run in
     * {@link RTPSourceStream#thread} in order to transfer data out of the
     * RTPSourceStream while, optionally, keeping a
     * WeakReference to the RTPSourceStream.
     *
     * @author Lyubomir Marinov
     */
    private static class TransferDataRunnable
        implements Runnable
    {
        /**
         * The indicator which determines whether TransferDataRunnable
         * keeps a WeakReference to the associated
         * RTPSourceStream.
         */
        private static final boolean WEAK_REFERENCE = false;

        /**
         * The RTPSourceStream which has initialized and owns this
         * instance.
         */
        private final RTPSourceStream owner;

        /**
         * The (unique) reason for invoking
         * {@link BufferTransferHandler#transferData(PushBufferStream)} on
         * {@link #owner}. Introduced in order to prevent busy waits when there
         * are enough packets to be read without blocking but no read is
         * actually performed.
         */
        private long transferDataReason;

        /**
         * A WeakReference to {@link #owner}.
         */
        private final WeakReference weakReference;

        /**
         * Initializes a new TransferDataRunnable instance which is to
         * transfer data out of a specific RTPSourceStream.
         *
         * @param owner the RTPSourceStream which is initializing the
         * new instance
         */
        public TransferDataRunnable(RTPSourceStream owner)
        {
            if (WEAK_REFERENCE)
            {
                this.owner = null;
                this.weakReference = new WeakReference(owner);
            }
            else
            {
                this.owner = owner;
                this.weakReference = null;
            }
        }

        /**
         * Gets the RTPSourceStream which has initialized and owns this
         * instance.
         *
         * @return the RTPSourceStream which has initialized and owns
         * this instance
         */
        private RTPSourceStream getOwner()
        {
            return WEAK_REFERENCE ? weakReference.get() : owner;
        }

        @Override
        public void run()
        {
            try
            {
                do
                {
                    RTPSourceStream owner = getOwner();

                    if ((owner == null) || !owner.runInThread(this))
                        break;
                }
                while (true);
            }
            finally
            {
                RTPSourceStream owner = getOwner();

                if (owner != null)
                    owner.threadExited(this);
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy