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

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

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

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

import net.sf.fmj.media.*;

/**
 * Implements JitterBufferBehaviour for audio media data. It realizes
 * a jitter buffer which is either fixed or adaptive.
 *
 * @author Lyubomir Marinov
 */
class AudioJitterBufferBehaviour
    extends BasicJitterBufferBehaviour
{
    private final static int DEFAULT_AUD_PKT_SIZE = 256;

    /**
     * The default duration in milliseconds of an audio RTP packet. The
     * default value expresses an expectation only. A value of 20
     * seems more reasonable than, for example, 30 because it is
     * more commonly used in specifications.
     */
    private static final int DEFAULT_MS_PER_PKT = 20;

    /**
     * The number of initial RTP packets AudioJitterBufferBehaviour is
     * to ignore before commencing the procedure of adapting.
     */
    private static final int INITIAL_PACKETS = 300;

    private static final AudioFormat MPEG = new AudioFormat("mpegaudio/rtp");

    /**
     * Whether resizing the queue is enabled.
     */
    private final boolean AJB_ENABLED;

    /**
     * How many packets to increment the queue size by, when growing.
     */
    private final int AJB_GROW_INCREMENT;

    /**
     * How many packets to monitor when deciding whether to grow the queue.
     */
    private final int AJB_GROW_INTERVAL;

    /**
     * Grow the queue if there are at least that many late packets (in the
     * last AJB_GROW_INTERVAL packets)
     */
    private final int AJB_GROW_THRESHOLD;

    /**
     * The maximum size/capacity to which this instance will grow the associated
     * RTP packet queue/jitter buffer.
     */
    private final int AJB_MAX_SIZE;

    /**
     * The minimum size/capacity to which this instance will shrink the
     * associated RTP packet queue/jitter buffer.
     */
    private final int AJB_MIN_SIZE;

    /**
     * The number of packets by which this instance is to shrink the associated
     * RTP packet queue/jitter buffer at a time.
     */
    private final int AJB_SHRINK_DECREMENT;

    /**
     * The number of packets during the receipt of which statistics are to be
     * gathered for the purposes of shrinking.
     */
    private final int AJB_SHRINK_INTERVAL;

    /**
     * The number of packets to be monitored whether they represent unnecessary
     * latency so that a decision to shrink by {@link #AJB_SHRINK_DECREMENT} may
     * be made.
     */
    private final int AJB_SHRINK_THRESHOLD;

    /**
     * Contains the number of 'late' packets from the last
     * AJB_GROW_INTERVAL packets. Updated on every add().
     */
    private int growCount;

    /**
     * Contains information about the recently received packets. A
     * 0 indicates that the respective packet was accepted
     * normally, a 1 indicates that it was dropped because it was
     * received too late. The storage of the history is circular
     * and {@link #historyPointer} always points to the last packet added.
     */
    private byte[] history;

    /**
     * The number of packets for which history is valid. Used in
     * order to avoid filling history with zeroes when it needs to
     * be reset.
     */
    private int historyLength;

    /**
     * Points to the place in history corresponding to the last
     * packet added
     */
    private int historyTail;

    /**
     * The average approximation of the duration in milliseconds of an RTP
     * packet. Used for audio only at the time of this writing. It sounds
     * reasonable to introduce such a value for the duration since there is
     * one for the size in bytes already (i.e. sizePerPkt).
     */
    private long msPerPkt = DEFAULT_MS_PER_PKT;

    private boolean replenish = true;

    /**
     * The number of packets which have been received since the last/previous
     * {@link #AJB_SHRINK_INTERVAL} has elapsed i.e. the time expressed in
     * number of packets which has elapsed from the current
     * AJB_SHRINK_INTERVAL. At the end of the interval in question, a
     * decision will be made whether the associated queue will be shrunk.
     */
    private int shrinkCount = 0;

    /**
     * The indicator which determines whether the Buffer.FLAG_SKIP_FEC
     * flag should be set on the next packet read from/out of the RTP packet
     * queue.
     */
    private boolean skipFec = false;

    /**
     * Initializes a new AudioJitterBufferBehaviour instance for the
     * purposes of a specific RTPSourceStream.
     *
     * @param stream the RTPSourceStream which has requested the
     * initialization of the new instance
     */
    public AudioJitterBufferBehaviour(RTPSourceStream stream)
    {
        super(stream);

        // Assign the adaptive jitter buffer-related properties of this instance
        // values from the Registry or default values.
        AJB_ENABLED
            = com.sun.media.util.Registry.getBoolean(
                    "adaptive_jitter_buffer_ENABLE",
                    true);
        AJB_GROW_INCREMENT
            = com.sun.media.util.Registry.getInt(
                    "adaptive_jitter_buffer_GROW_INCREMENT",
                    2);
        AJB_GROW_INTERVAL
            = com.sun.media.util.Registry.getInt(
                    "adaptive_jitter_buffer_GROW_INTERVAL",
                    30);
        AJB_GROW_THRESHOLD
            = com.sun.media.util.Registry.getInt(
                    "adaptive_jitter_buffer_GROW_THRESHOLD",
                    3);
        AJB_MAX_SIZE
            = com.sun.media.util.Registry.getInt(
                    "adaptive_jitter_buffer_MAX_SIZE",
                    16);
        AJB_MIN_SIZE
            = com.sun.media.util.Registry.getInt(
                    "adaptive_jitter_buffer_MIN_SIZE",
                    4);
        AJB_SHRINK_DECREMENT
            = com.sun.media.util.Registry.getInt(
                    "adaptive_jitter_buffer_SHRINK_DECREMENT",
                    1);
        AJB_SHRINK_INTERVAL
            = com.sun.media.util.Registry.getInt(
                    "adaptive_jitter_buffer_SHRINK_INTERVAL",
                    120);
        AJB_SHRINK_THRESHOLD
            = com.sun.media.util.Registry.getInt(
                    "adaptive_jitter_buffer_SHRINK_THRESHOLD",
                    1);

        initHistory();
    }

    /**
     * {@inheritDoc}
     *
     * Sets the value of {@link #skipFec} to true.
     */
    @Override
    public void dropPkt()
    {
        super.dropPkt();

        // We've deliberately dropped a packet since we're full. If FEC is
        // extracted from the next packet, we are likely to be in the same
        // situation again very soon. So avoid FEC being decoded from the next
        // packet read.
        skipFec = true;
        if (q.getFillCount() < AJB_SHRINK_THRESHOLD)
            shrinkCount = 0;
    }

    /**
     * {@inheritDoc}
     *
     * If this JitterBufferBehaviour is adaptive, computes the absolute
     * maximum delay based on {@link #AJB_MAX_SIZE}.
     */
    @Override
    public int getAbsoluteMaximumDelay()
    {
        long absoluteMaximumDelay;

        if (isAdaptive())
        {
            long msPerPkt = this.msPerPkt;

            if (msPerPkt <= 0)
                msPerPkt = DEFAULT_MS_PER_PKT;
            absoluteMaximumDelay = AJB_MAX_SIZE * msPerPkt;
        }
        else
        {
            absoluteMaximumDelay = super.getAbsoluteMaximumDelay();
        }
        return
            (absoluteMaximumDelay > 65535) ? 65535 : (int) absoluteMaximumDelay;
    }

    /**
     * {@inheritDoc}
     *
     * Computes the maximum delay based on the capacity of the
     * JitterBuffer/{@link #q}.
     */
    @Override
    public int getMaximumDelay()
    {
        long msPerPkt = this.msPerPkt;

        if (msPerPkt <= 0)
            msPerPkt = DEFAULT_MS_PER_PKT;

        long maximumDelay = q.getCapacity() * msPerPkt;

        return (maximumDelay > 65535) ? 65535 : (int) maximumDelay;
    }

    /**
     * {@inheritDoc}
     *
     * Computes the nominal delay based on the capacity of the
     * JitterBuffer/{@link #q} and knowing that a {@link #replenish}
     * requires a half of that capacity.
     */
    @Override
    public int getNominalDelay()
    {
        long msPerPkt = this.msPerPkt;

        if (msPerPkt <= 0)
            msPerPkt = DEFAULT_MS_PER_PKT;

        long nominalDelay = (q.getCapacity() / 2) * msPerPkt;

        return (nominalDelay > 65535) ? 65535 : (int) nominalDelay;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void grow(int capacity)
    {
        super.grow(capacity);

        resetHistory();
    }

    /**
     * Initializes the history
     */
    private void initHistory()
    {
        history = new byte[AJB_GROW_INTERVAL];
        historyLength = 0;
        historyTail = 0;
        growCount = 0;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isAdaptive()
    {
        return AJB_ENABLED;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected int monitorQSize(Buffer buffer)
    {
        super.monitorQSize(buffer);

        if (AJB_ENABLED)
        {
            int size = q.getCapacity();

            if ((historyLength >= AJB_GROW_INTERVAL)
                    && (growCount >= AJB_GROW_THRESHOLD)
                    && (size < AJB_MAX_SIZE))
            {
                int n = Math.min(size + AJB_GROW_INCREMENT, AJB_MAX_SIZE);
                if (n > size)
                    grow(n);
            }

            shrinkCount++;
            // The queue will not be shrunk if it will be unable to accommodate
            // the specified buffer afterwards.
            if ((shrinkCount >= AJB_SHRINK_INTERVAL)
                    && (size > AJB_MIN_SIZE)
                    && q.freeNotEmpty())
            {
                int n = Math.max(size - AJB_SHRINK_DECREMENT, AJB_MIN_SIZE);
                if (n < size)
                    shrink(n);
            }
        }

        BufferControl bc = getBufferControl();

        if (bc == null)
            return 0;
        else
        {
            Format format = stream.getFormat();
            long ms;
            int sizePerPkt = stats.getSizePerPacket();
            if (sizePerPkt <= 0)
                sizePerPkt = DEFAULT_AUD_PKT_SIZE;
            if (MPEG.matches(format))
                ms = sizePerPkt / 4;
            else
            {
                ms = DEFAULT_MS_PER_PKT;
                try
                {
                    long ns = buffer.getDuration();

                    if (ns <= 0)
                    {
                        ns
                            = ((AudioFormat) format).computeDuration(
                                    buffer.getLength());
                        if (ns > 0)
                            ms = ns / 1000000L;
                    }
                    else
                        ms = ns / 1000000L;
                }
                catch (Throwable t)
                {
                    if (t instanceof ThreadDeath)
                        throw (ThreadDeath) t;
                }
            }
            msPerPkt = (msPerPkt + ms) / 2;
            ms = (msPerPkt == 0) ? DEFAULT_MS_PER_PKT : msPerPkt;
            int aprxBufferLengthInPkts
                = (int) (bc.getBufferLength() / ms);

            // If the adaptive jitter buffer mode is enabled, we let this queue
            // manage its size, ignoring bc. Otherwise, we adapt to the value of
            // bc (which was the behavior before resizing based on the history
            // of late packets was introduced here).
            if (!AJB_ENABLED && (aprxBufferLengthInPkts > q.getCapacity()))
            {
                grow(aprxBufferLengthInPkts);
                int size = q.getCapacity();
                Log.comment(
                        "Grew audio RTP packet queue to: " + size + " pkts, "
                            + size * sizePerPkt + " bytes.\n");
            }
            return aprxBufferLengthInPkts;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean preAdd(Buffer buffer, RTPRawReceiver rtprawreceiver)
    {
        long lastSeqSent = stream.getLastReadSequenceNumber();
        long bufferSN;

        if ((lastSeqSent != Buffer.SEQUENCE_UNKNOWN)
                && ((bufferSN = buffer.getSequenceNumber()) < lastSeqSent))
        {
            // A packet which is subsequent to the specified buffer has already
            // been read. It should be added to the history so that the queue
            // may be resized if necessary. But if it is late by more than
            // AJB_MAX_SIZE, it is too late to take it into account and,
            // consequently, is ignored.
            if(lastSeqSent - bufferSN < AJB_MAX_SIZE)
            {
                recordInHistory(true);
                stats.incrementDiscardedLate();
            }
            else
                stats.incrementDiscardedVeryLate();
            return false;
        }

        recordInHistory(false);

        if (!super.preAdd(buffer, rtprawreceiver))
            return false;

        // If the queue is full and it hasn't reached it's maximum size, grow
        // it. This is to adapt to groups of packets arriving in a short period
        // of time.

        // During the first few seconds after a stream is started, the queue is
        // often observed to be full. But this is likely not due to bursts of
        // packets from the network, so we shouldn't try to adapt. Hence the
        // INITIAL_PACKETS check.
        if (AJB_ENABLED
                && q.noMoreFree()
                && (stats.getNbAdd() > INITIAL_PACKETS))
        {
            int size = q.getCapacity();
            if (size < AJB_MAX_SIZE)
            {
                // There is still room for the queue to grow and to not drop
                // packets.
                grow(Math.min(size * 2, AJB_MAX_SIZE));
            }
            else
            {
                // The queue cannot grow any further so at least one packet has
                // to be dropped. However, dropping a single packet will very
                // likely be insufficient. In order to maximize the chances of
                // bettering the situation, re-center.
                while (q.getFillCount() >= (size / 2))
                {
                    stats.incrementDiscardedFull();
                    dropPkt();
                }
            }
        }

        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void read(Buffer buffer)
    {
        super.read(buffer);

        if (!buffer.isDiscard() && skipFec)
        {
            buffer.setFlags(buffer.getFlags() | Buffer.FLAG_SKIP_FEC);
            skipFec = false;
        }

        int totalPkts = q.getFillCount();

        if (totalPkts == 0)
            replenish = true;
        if (totalPkts < AJB_SHRINK_THRESHOLD)
            shrinkCount = 0;
    }

    /**
     * Records a packet in history.
     *
     * @param late whether the packet arrived too late or not
     */
    private void recordInHistory(boolean late)
    {
        int n = late ? 1 : 0;

        growCount += n - history[historyTail];

        history[historyTail] = (byte) n;
        historyTail = (historyTail + 1 ) % AJB_GROW_INTERVAL;

        if (historyLength < AJB_GROW_INTERVAL)
            historyLength++;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void reset()
    {
        super.reset();

        resetHistory();
    }

    /**
     * Resets the history.
     */
    private void resetHistory()
    {
        historyLength = 0;
        shrinkCount = 0;
    }

    /**
     * Resizes the queue to capacity. Assumes capacity is
     * not more than the current capacity of the queue. Drops packets if
     * necessary.
     */
    private void shrink(int capacity)
    {
        if (capacity < 1)
            throw new IllegalArgumentException("capacity");

        int qCapacity = q.getCapacity();

        if (capacity == qCapacity)
            return;
        if (capacity > qCapacity)
            throw new IllegalArgumentException("capacity");

        Log.info("Shrinking packet queue to " + capacity);

        int dropped = 0;

        while (q.getFillCount() > capacity)
        {
            dropPkt();
            stats.incrementDiscardedShrink();
            ++dropped;
        }
        q.setCapacity(capacity);

        // The shrinking as it is implemented at the time of this writing is
        // attempted in order to drop at least AJB_SHRINK_DECREMENT packets from
        // the queue.
        while ((dropped < AJB_SHRINK_DECREMENT) && q.fillNotEmpty())
        {
            dropPkt();
            stats.incrementDiscardedShrink();
            ++dropped;
        }

        resetHistory();
    }

    /**
     * @Override
     */
    @Override
    public boolean willReadBlock()
    {
        boolean b = super.willReadBlock();

        if (!b)
        {
            if (replenish && (q.getFillCount() >= (q.getCapacity() / 2)))
                replenish = false;
            b = replenish;
        }
        return b;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy