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

org.lastbamboo.common.ice.IceMediaStreamImpl Maven / Gradle / Ivy

package org.lastbamboo.common.ice;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Queue;

import org.apache.commons.lang.math.RandomUtils;
import org.lastbamboo.common.ice.candidate.IceCandidate;
import org.lastbamboo.common.ice.candidate.IceCandidateGatherer;
import org.lastbamboo.common.ice.candidate.IceCandidatePair;
import org.lastbamboo.common.ice.candidate.IceCandidatePairState;
import org.lastbamboo.common.ice.candidate.IceUdpPeerReflexiveCandidate;
import org.lastbamboo.common.ice.sdp.IceCandidateSdpEncoder;
import org.lastbamboo.common.offer.answer.IceMediaStreamDesc;
import org.littleshoot.stun.stack.message.BindingRequest;
import org.littleshoot.stun.stack.message.attributes.StunAttribute;
import org.littleshoot.stun.stack.message.attributes.StunAttributeType;
import org.littleshoot.stun.stack.message.attributes.ice.IcePriorityAttribute;
import org.littleshoot.util.Closure;
import org.littleshoot.util.Predicate;
import org.littleshoot.util.mina.MinaUtils;
import org.littleshoot.mina.common.IoHandler;
import org.littleshoot.mina.common.IoService;
import org.littleshoot.mina.common.IoServiceConfig;
import org.littleshoot.mina.common.IoSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Class containing an ICE media stream.  Each media stream contains a single
 * ICE check list, as described in ICE section 5.7.
 */
public class IceMediaStreamImpl implements IceMediaStream {

    private final Logger m_log = LoggerFactory.getLogger(getClass());
    private IceCheckList m_checkList;
    
    private final Queue m_validPairs =
        new PriorityQueue();
    
    /**
     * {@link Queue} of nominated pairs for this check list.
     */
    private final Queue m_nominatedPairs = 
        new PriorityQueue();

    private Collection m_localCandidates;
    private final IceAgent m_iceAgent;
    private final IceMediaStreamDesc m_desc;
    private final Collection m_remoteCandidates = 
        new ArrayList(10);
    private Collection m_remoteSdpCandidates = 
        new ArrayList(10);
    private final IceCandidateGatherer m_gatherer;
    private IceCheckScheduler m_checkScheduler;
    private boolean m_closed;
    private final IceStunUdpPeer m_udpPeer;
    
    /**
     * Creates a new ICE media stream.
     * 
     * @param iceAgent The ICE agent.
     * @param streamDesc The description of the stream to create.
     * @param gatherer The class that will gather ICE candidates for the 
     * stream.
     */
    public IceMediaStreamImpl(final IceAgent iceAgent,
            final IceMediaStreamDesc streamDesc,
            final IceCandidateGatherer gatherer, final IceStunUdpPeer udpPeer) {
        m_iceAgent = iceAgent;
        m_desc = streamDesc;
        m_gatherer = gatherer;
        this.m_udpPeer = udpPeer;
    }

    public void start(final IceCheckList checkList,
            final Collection localCandidates,
            final IceCheckScheduler scheduler) {
        this.m_localCandidates = localCandidates;
        this.m_checkList = checkList;
        this.m_checkScheduler = scheduler;
    }

    public byte[] encodeCandidates() {
        final IceCandidateSdpEncoder encoder = new IceCandidateSdpEncoder(
                m_desc.getMimeContentType(), m_desc.getMimeContentSubtype());
        encoder.visitCandidates(getLocalCandidates());
        return encoder.getSdp();
    }

    public void establishStream(final Collection remoteCandidates) {
        synchronized (this.m_remoteCandidates) {
            synchronized (this.m_remoteSdpCandidates) {
                synchronized (remoteCandidates) {
                    this.m_remoteCandidates.addAll(remoteCandidates);
                    this.m_remoteSdpCandidates.addAll(remoteCandidates);
                    this.m_remoteSdpCandidates = Collections
                            .unmodifiableCollection(this.m_remoteSdpCandidates);
                }
            }
        }

        m_checkList.formCheckList(remoteCandidates);

        processPairGroups();

        if (this.m_closed) {
            m_log.info("Already closed - not scheduling checks!!");
            return;
        }

        this.m_checkScheduler.scheduleChecks();
        m_checkList.check();
    }

    public IceCandidate addRemotePeerReflexive(final BindingRequest request,
            final InetSocketAddress localAddress,
            final InetSocketAddress remoteAddress, final boolean isUdp) {
        // See ICE section 7.2.1.3.
        final Map attributes = request
                .getAttributes();
        final IcePriorityAttribute priorityAttribute = (IcePriorityAttribute) attributes
                .get(StunAttributeType.ICE_PRIORITY);
        final long priority = priorityAttribute.getPriority();

        // We set the foundation to a random value.
        final String foundation = String.valueOf(RandomUtils.nextLong());

        // Find the local candidate for the address the request was sent to.
        // We use that to determine the component ID of the new peer
        // reflexive candidate.
        final IceCandidate localCandidate = getLocalCandidate(localAddress,
                isUdp);
        if (localCandidate == null) {
            // We need to synchronized for toString to work on local candidates.
            synchronized (this) {
                m_log.warn("Could not find local candidate " + localAddress
                        + " in: " + this.getLocalCandidates() + ".  Aborting.");
            }
            return null;
        }
        final int componentId = localCandidate.getComponentId();

        m_log.debug("Creating new peer reflexive candidate");
        final IceCandidate prc = new IceUdpPeerReflexiveCandidate(
                remoteAddress, foundation, componentId,
                this.m_iceAgent.isControlling(), priority);

        addRemoteCandidate(prc);

        return prc;
    }

    private void addRemoteCandidate(final IceCandidate candidate) {
        synchronized (this.m_remoteCandidates) {
            this.m_remoteCandidates.add(candidate);
        }
    }

    /**
     * Groups the pairs as specified in ICE section 5.7.4. The purpose of this
     * grouping appears to be just to set the establish the waiting pair for
     * each foundation prior to running connectivity checks.
     * 
     * @param pairs The pairs to form into foundation-based groups for setting 
     * the state of the pair with the lowest component ID to waiting.
     */
    private void processPairGroups() {
        final Map> groupsMap = 
            new HashMap>();

        // Group together pairs with the same foundation.
        final Closure groupClosure = new Closure() {
            public void execute(final IceCandidatePair pair) {
                final String foundation = pair.getFoundation();
                final List foundationPairs;
                if (groupsMap.containsKey(foundation)) {
                    foundationPairs = groupsMap.get(foundation);
                } else {
                    foundationPairs = new LinkedList();
                    groupsMap.put(foundation, foundationPairs);
                }
                foundationPairs.add(pair);
            }
        };
        this.m_checkList.executeOnPairs(groupClosure);

        final Collection> groups = groupsMap.values();

        m_log.debug(groups.size() + " before sorting...");
        for (final List group : groups) {
            setLowestComponentIdToWaiting(group);
        }
    }

    private void setLowestComponentIdToWaiting(
            final List pairs) {
        IceCandidatePair pairToSet = null;
        for (final IceCandidatePair pair : pairs) {
            if (pairToSet == null) {
                pairToSet = pair;
                continue;
            }

            // Always use the lowest component ID.
            if (pair.getComponentId() < pairToSet.getComponentId()) {
                pairToSet = pair;
            }

            // If the component IDs match, use the one with the highest
            // priority. See ICE section 5.7.4
            else if (pair.getComponentId() == pairToSet.getComponentId()) {
                if (pair.getPriority() > pairToSet.getPriority()) {
                    pairToSet = pair;
                }
            }
        }

        if (pairToSet != null) {
            pairToSet.setState(IceCandidatePairState.WAITING);
        } else {
            m_log.warn("No pair to set!!!");
        }
    }

    public Queue getValidPairs() {
        return m_validPairs;
    }

    public void addLocalCandidate(final IceCandidate localCandidate) {
        synchronized (this.getLocalCandidates()) {
            this.getLocalCandidates().add(localCandidate);
        }
    }

    public IceCandidate getLocalCandidate(final InetSocketAddress localAddress,
            final boolean isUdp) {
        return getCandidate(this.getLocalCandidates(), localAddress, isUdp);
    }

    public IceCandidate getRemoteCandidate(
            final InetSocketAddress remoteAddress, final boolean isUdp) {
        return getCandidate(this.m_remoteCandidates, remoteAddress, isUdp);
    }

    public boolean hasRemoteCandidate(final InetSocketAddress remoteAddress,
            final boolean isUdp) {
        final IceCandidate remoteCandidate = getCandidate(
                this.m_remoteCandidates, remoteAddress, isUdp);

        return remoteCandidate != null;
    }

    public boolean hasRemoteCandidateInSdp(
            final InetSocketAddress remoteAddress, final boolean isUdp) {
        final IceCandidate remoteCandidate = getCandidate(
                this.m_remoteSdpCandidates, remoteAddress, isUdp);

        return remoteCandidate != null;
    }

    private IceCandidate getCandidate(
            final Collection candidates,
            final InetSocketAddress address, final boolean isUdp) {
        // A little inefficient here, but we're not talking about a lot of
        // candidates.
        synchronized (candidates) {
            for (final IceCandidate candidate : candidates) {
                if (candidate.isUdp()) {
                    if (!isUdp)
                        continue;
                } else {
                    if (isUdp)
                        continue;
                }
                if (address.equals(candidate.getSocketAddress())) {
                    return candidate;
                }
            }
            m_log.debug(address + " with transport: " + (isUdp ? "UDP" : "TCP")
                    + " not found in " + candidates);
        }

        return null;
    }

    public IceCandidatePair getPair(final InetSocketAddress localAddress,
            final InetSocketAddress remoteAddress, final boolean isUdp) {
        // The check list might not exist yet if the offerer receives incoming
        // requests before it has received an answer.
        if (this.m_checkList == null) {
            return null;
        }
        final Predicate pred = new Predicate() {
            public boolean evaluate(final IceCandidatePair pair) {
                final IceCandidate lc = pair.getLocalCandidate();
                final IceCandidate rc = pair.getRemoteCandidate();
                if ((isUdp && lc.isUdp()) || (!isUdp && !lc.isUdp())) {
                    if (lc.getSocketAddress().equals(localAddress)
                            && rc.getSocketAddress().equals(remoteAddress)) {
                        return true;
                    }
                }
                return false;
            }
        };

        return this.m_checkList.selectAnyPair(pred);
    }

    public void updatePairStates(final IceCandidatePair generatingPair) {
        m_log.debug("Updating pair states...");
        // Set the state of the pair that *generated* the check to succeeded.
        generatingPair.setState(IceCandidatePairState.SUCCEEDED);

        // Now set FROZEN pairs with the same foundation as the pair that
        // *generated* the check for this media stream to waiting.
        updateToWaiting(generatingPair);
    }
    
    public void updateCheckListAndTimerStates() {
        // Update check list and timer states. See section 7.1.2.3.
        if (allFailedOrSucceeded()) {
            // 1) Set the check list to failed if there is not a pair in the
            // valid list for all components.
            m_log.debug("All check lists are either failed or succeeded");

            // TODO: We only currently have one component!!
            if (this.m_validPairs.isEmpty()) {
                // The check list is definitely created at this point, as
                // we're updating pair state for a pair that had to have
                // been on the check list.
                m_log.debug("Setting check list state to failed...");
                this.m_checkList.setState(IceCheckListState.FAILED);
            }

            // 2) Agent changes state of pairs in frozen check lists.
            this.m_iceAgent.onUnfreezeCheckLists(this);
        }

        // The final part of this section states the following:
        //
        // If none of the pairs in the check list are in the Waiting or Frozen
        // state, the check list is no longer considered active, and will not
        // count towards the value of N in the computation of timers for
        // ordinary checks as described in Section 5.8.

        // NOTE: This requires no action on our part. The definition of
        // and "active" check list is "a check list with at least one pair
        // that is Waiting" from 5.7.4. When computing the value of N, that's
        // the definition that's used, and the active state is determined
        // dynamically at that time.
    }

    /**
     * Checks to see if all pairs are in either the SUCCEEDED or the FIALED
     * state.
     * 
     * @return true if all pairs are in either the SUCCEEDED or
     * the FAILED state, otherwise false.
     */
    private boolean allFailedOrSucceeded() {
        final Predicate pred = new Predicate() {

            public boolean evaluate(final IceCandidatePair pair) {
                if (pair.getState() != IceCandidatePairState.SUCCEEDED
                        && pair.getState() != IceCandidatePairState.FAILED) {
                    return false;
                }
                return true;
            }
        };

        return this.m_checkList.matchesAll(pred);
    }

    public void addValidPair(final IceCandidatePair pair) {
        synchronized (this.m_validPairs) {
            this.m_validPairs.add(pair);
        }
    }
    
    /**
     * Implements part 1 of "7.1.2.2.3. Updating Pair States."  All pairs with
     * the same foundation as the successful pair that are in the FROZEN state
     * are switched to the WAITING state.
     *  
     * @param successfulPair The pair that succeeded.
     */
    private void updateToWaiting(final IceCandidatePair successfulPair) {
        // TODO: This should happen for ALL components. We only currently
        // support one component. See:
        // http://tools.ietf.org/html/draft-ietf-mmusic-ice-17#section-7.1.2.2.3
        final Closure closure = new Closure() {
            public void execute(final IceCandidatePair pair) {
                // We just update pairs with the same foundation that are in
                // the frozen state to the waiting state.
                if (pair.getFoundation().equals(successfulPair.getFoundation())
                        && pair.getState() == IceCandidatePairState.FROZEN) {
                    pair.setState(IceCandidatePairState.WAITING);
                }
            }
        };
        this.m_checkList.executeOnPairs(closure);
    }

    public void recomputePairPriorities(final boolean controlling) {
        this.m_checkList.recomputePairPriorities(controlling);
    }

    public void addTriggeredPair(final IceCandidatePair pair) {
        if (this.m_iceAgent.getIceState() == IceState.COMPLETED) {
            m_log.debug("Pair already nominated...not adding pair");
        } else {
            m_log.debug("Adding triggered pair to media stream: {}", this);
            this.m_checkList.addTriggeredPair(pair);

            // This call notifies the scheduler to start scheduling again in
            // the case where we've run out of pairs.
            this.m_checkScheduler.onPair();
        }
    }

    public void addPair(final IceCandidatePair pair) {
        if (this.m_iceAgent.getIceState() == IceState.COMPLETED) {
            m_log.debug("Pair already nominated...not adding pair");
        } else {
            m_log.debug("Adding pair to media stream: {}", this);
            this.m_checkList.addPair(pair);
        }
    }

    public IceCheckListState getCheckListState() {
        return this.m_checkList.getState();
    }

    public void setCheckListState(final IceCheckListState state) {
        this.m_checkList.setState(state);
        if (state == IceCheckListState.COMPLETED) {
            // this.m_checkScheduler.
        }
    }

    public boolean hasHigherPriorityPendingPair(final IceCandidatePair pair) {
        return this.m_checkList.hasHigherPriorityPendingPair(pair);
    }

    public void onNominated(final IceCandidatePair pair) {
        if (pair == null) {
            throw new NullPointerException("Can't nominate null pair");
        }
        // First, remove all Waiting and Frozen pairs on the check list and
        // triggered check queue.
        this.m_checkList.removeWaitingAndFrozenPairs(pair);
        synchronized (this.m_nominatedPairs) {
            this.m_nominatedPairs.add(pair);
        }
    }

    public Queue getNominatedPairs() {
        // Return a copy of the pairs to maintain immutability.
        synchronized (this.m_nominatedPairs) {
            final Queue pairs = new PriorityQueue();
            pairs.addAll(this.m_nominatedPairs);
            return pairs;
        }
    }

    public void serviceActivated(final IoService service,
            final SocketAddress serviceAddress, final IoHandler handler,
            final IoServiceConfig config) {
    }

    public void serviceDeactivated(final IoService service,
            final SocketAddress serviceAddress, final IoHandler handler,
            final IoServiceConfig config) {
    }

    public void sessionCreated(final IoSession session) {
        m_log.debug("Setting media stream on session");
        if (m_closed) {
            m_log.info("Already closed. Closing session.");
            session.close();
            return;
        }
        session.setAttribute(IceMediaStream.class.getSimpleName(), this);

        final InetSocketAddress localAddress = (InetSocketAddress) session
                .getLocalAddress();
        final InetSocketAddress remoteAddress = (InetSocketAddress) session
                .getRemoteAddress();

        final boolean isUdp = MinaUtils.isUdp(session);
        final IceCandidatePair pair = getPair(localAddress, remoteAddress,
                isUdp);

        if (pair == null) {
            return;
        }
        if (pair.getIoSession() == null) {
            pair.setIoSession(session);
        }
    }

    public void sessionDestroyed(final IoSession session) {
    }

    public void close() {
        this.m_closed = true;
        this.m_checkList.close();
        this.m_gatherer.close();
    }

    public Collection getLocalCandidates() {
        synchronized (this.m_localCandidates) {
            return new ArrayList(this.m_localCandidates);
        }
    }

    public InetAddress getPublicAddress() {
        return this.m_gatherer.getPublicAddress();
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + " controlling: "
                + this.m_iceAgent.isControlling();
    }

    public IceStunUdpPeer getStunUdpPeer() {
        return this.m_udpPeer;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy