sas.systems.imflux.session.rtp.AbstractRtpSession.java Source code

Java tutorial

Introduction

Here is the source code for sas.systems.imflux.session.rtp.AbstractRtpSession.java

Source

/*
 * Copyright 2016 Sebastian Schmidl
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package sas.systems.imflux.session.rtp;

import java.math.BigInteger;
import java.net.SocketAddress;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.AddressedEnvelope;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.DefaultAddressedEnvelope;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.oio.OioEventLoopGroup;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.socket.oio.OioDatagramChannel;
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import sas.systems.imflux.logging.Logger;
import sas.systems.imflux.network.udp.UdpControlHandler;
import sas.systems.imflux.network.udp.UdpControlPacketDecoder;
import sas.systems.imflux.network.udp.UdpControlPacketEncoder;
import sas.systems.imflux.network.udp.UdpDataHandler;
import sas.systems.imflux.network.udp.UdpDataPacketDecoder;
import sas.systems.imflux.network.udp.UdpDataPacketEncoder;
import sas.systems.imflux.packet.DataPacket;
import sas.systems.imflux.packet.rtcp.AbstractReportPacket;
import sas.systems.imflux.packet.rtcp.AppDataPacket;
import sas.systems.imflux.packet.rtcp.ByePacket;
import sas.systems.imflux.packet.rtcp.CompoundControlPacket;
import sas.systems.imflux.packet.rtcp.ControlPacket;
import sas.systems.imflux.packet.rtcp.ReceiverReportPacket;
import sas.systems.imflux.packet.rtcp.ReceptionReport;
import sas.systems.imflux.packet.rtcp.SdesChunk;
import sas.systems.imflux.packet.rtcp.SdesChunkItems;
import sas.systems.imflux.packet.rtcp.SenderReportPacket;
import sas.systems.imflux.packet.rtcp.SourceDescriptionPacket;
import sas.systems.imflux.participant.ParticipantDatabase;
import sas.systems.imflux.participant.ParticipantOperation;
import sas.systems.imflux.participant.RtpParticipant;
import sas.systems.imflux.participant.RtpParticipantInfo;

/**
 * Defines standard and common functionality for a RTCP/RTP session. A RTP session 
 * manages two channels:
 * <ul>
 *    <li>{@link #dataChannel} for data exchange</li>
 *    <li>{@link #controlChannel} for control commands</li>
 * </ul>
 * <p>
 * This class has a default RTCP handling implementation, which is used by default.<br/>
 * You can deactivate this functionality with {@code setAutomatedRtcpHandling(false)}.
 * TODO: describe RTCP handling
 * </p>
 * 
 * @author <a href="https://github.com/CodeLionX">CodeLionX</a>
 */
public abstract class AbstractRtpSession implements RtpSession, TimerTask {

    // constants ------------------------------------------------------------------------------------------------------
    protected static final Logger LOG = Logger.getLogger(AbstractRtpSession.class);
    protected static final String VERSION = "imflux_0.1.1_16052016";

    // configuration defaults -----------------------------------------------------------------------------------------
    protected static final boolean USE_NIO = true;
    protected static final boolean DISCARD_OUT_OF_ORDER = true;
    protected static final int BANDWIDTH_LIMIT = 256;
    protected static final int SEND_BUFFER_SIZE = 1500;
    protected static final int RECEIVE_BUFFER_SIZE = 1500;
    protected static final int MAX_COLLISIONS_BEFORE_CONSIDERING_LOOP = 3;
    protected static final boolean AUTOMATED_RTCP_HANDLING = true;
    protected static final boolean TRY_TO_UPDATE_ON_EVERY_SDES = true;
    protected static final int PARTICIPANT_DATABASE_CLEANUP = 10;

    // configuration --------------------------------------------------------------------------------------------------
    protected final String id;
    protected final int payloadType;
    protected final HashedWheelTimer timer;
    protected boolean useNio;
    protected boolean discardOutOfOrder;
    protected int bandwidthLimit;
    protected int sendBufferSize;
    protected int receiveBufferSize;
    protected int maxCollisionsBeforeConsideringLoop;
    protected boolean automatedRtcpHandling;
    protected boolean tryToUpdateOnEverySdes;
    protected int participantDatabaseCleanup;

    // internal vars --------------------------------------------------------------------------------------------------
    protected final AtomicBoolean running;
    protected final RtpParticipant localParticipant;
    protected final ParticipantDatabase participantDatabase;
    protected final List<RtpSessionDataListener> dataListeners;
    protected final List<RtpSessionControlListener> controlListeners;
    protected final List<RtpSessionEventListener> eventListeners;
    protected EventLoopGroup workerGroup;
    protected Channel dataChannel;
    protected Channel controlChannel;
    protected final AtomicInteger sequence;
    protected final AtomicBoolean sentOrReceivedPackets;
    protected final AtomicInteger collisions;
    protected final AtomicLong sentByteCounter;
    protected final AtomicLong sentPacketCounter;
    protected int periodicRtcpSendInterval;
    protected final boolean internalTimer;

    // constructors ---------------------------------------------------------------------------------------------------
    public AbstractRtpSession(String id, int payloadType, RtpParticipant local) {
        this(id, payloadType, local, null/*, null*/);
    }

    /**
     * 
     * @param id
     * @param payloadType 
     * @param local information about the local participant
     * @param timer timer for periodic RTCP report sending, if the timer is shared across the application
     */
    public AbstractRtpSession(String id, int payloadType, RtpParticipant local, HashedWheelTimer timer) {
        if ((payloadType < 0) || (payloadType > 127)) {
            throw new IllegalArgumentException("PayloadTypes must be in range [0;127]");
        }

        if (!local.isReceiver()) {
            throw new IllegalArgumentException("Local participant must have its data & control addresses set");
        }

        this.id = id;
        this.payloadType = payloadType;
        this.localParticipant = local;
        this.participantDatabase = this.createDatabase();
        if (timer == null) {
            this.timer = new HashedWheelTimer(1, TimeUnit.SECONDS);
            this.internalTimer = true;
        } else {
            this.timer = timer;
            this.internalTimer = false;
        }

        this.running = new AtomicBoolean(false);
        // CopyOnWriteArrayList to make this class thread-safe
        this.dataListeners = new CopyOnWriteArrayList<>();
        this.controlListeners = new CopyOnWriteArrayList<>();
        this.eventListeners = new CopyOnWriteArrayList<>();
        this.sequence = new AtomicInteger(0);
        this.sentOrReceivedPackets = new AtomicBoolean(false);
        this.collisions = new AtomicInteger(0);
        this.sentPacketCounter = new AtomicLong(0);
        this.sentByteCounter = new AtomicLong(0);

        this.useNio = USE_NIO;
        this.discardOutOfOrder = DISCARD_OUT_OF_ORDER;
        this.bandwidthLimit = BANDWIDTH_LIMIT;
        this.sendBufferSize = SEND_BUFFER_SIZE;
        this.receiveBufferSize = RECEIVE_BUFFER_SIZE;
        this.maxCollisionsBeforeConsideringLoop = MAX_COLLISIONS_BEFORE_CONSIDERING_LOOP;
        this.automatedRtcpHandling = AUTOMATED_RTCP_HANDLING;
        this.tryToUpdateOnEverySdes = TRY_TO_UPDATE_ON_EVERY_SDES;
        this.participantDatabaseCleanup = PARTICIPANT_DATABASE_CLEANUP;
    }

    // RtpSession -----------------------------------------------------------------------------------------------------
    /**
     * {@inheritDoc}
     */
    @Override
    public String getId() {
        return id;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getPayloadType() {
        return this.payloadType;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public synchronized boolean init() {
        if (this.running.get()) {
            return true;
        }
        Class<? extends Channel> channelType;

        if (useNio) {
            // create data channel bootstrap
            //          EventLoopGroup bossGroup = new NioEventLoopGroup(5, Executors.defaultThreadFactory()); // if we want to use others than the defaults
            this.workerGroup = new NioEventLoopGroup();
            channelType = NioDatagramChannel.class;
        } else {
            this.workerGroup = new OioEventLoopGroup();
            channelType = OioDatagramChannel.class;
        }

        Bootstrap dataBootstrap = new Bootstrap();
        dataBootstrap.group(this.workerGroup).option(ChannelOption.SO_SNDBUF, this.sendBufferSize)
                .option(ChannelOption.SO_RCVBUF, this.receiveBufferSize)
                // option not set: "receiveBufferSizePredictorFactory", new FixedReceiveBufferSizePredictorFactory(this.receiveBufferSize)
                .channel(channelType) // use an UDP channel implementation => forces us to use AddressedEnvelope
                .handler(new ChannelInitializer<Channel>() { // is used to initialize the ChannelPipeline
                    @Override
                    protected void initChannel(Channel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        pipeline.addLast("decoder", UdpDataPacketDecoder.getInstance());
                        pipeline.addLast("encoder", UdpDataPacketEncoder.getInstance());
                        pipeline.addLast("handler", new UdpDataHandler(AbstractRtpSession.this));
                    }
                });

        // create control channel bootstrap
        Bootstrap controlBootstrap = new Bootstrap();
        controlBootstrap.group(this.workerGroup).option(ChannelOption.SO_SNDBUF, this.sendBufferSize)
                .option(ChannelOption.SO_RCVBUF, this.receiveBufferSize)
                // option not set: "receiveBufferSizePredictorFactory", new FixedReceiveBufferSizePredictorFactory(this.receiveBufferSize)
                .channel(channelType) // use an UDP channel implementation => forces us to use AddressedEnvelope
                .handler(new ChannelInitializer<Channel>() { // is used to initialize the ChannelPipeline
                    @Override
                    protected void initChannel(Channel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        pipeline.addLast("decoder", UdpControlPacketDecoder.getInstance());
                        pipeline.addLast("encoder", UdpControlPacketEncoder.getInstance());
                        pipeline.addLast("handler", new UdpControlHandler(AbstractRtpSession.this));
                    }
                });

        // create data channel
        SocketAddress dataAddress = this.localParticipant.getDataDestination();
        try {
            ChannelFuture future = dataBootstrap.bind(dataAddress);
            this.dataChannel = future.sync().channel(); // wait for future to complete and retrieve channel
        } catch (Exception e) {
            LOG.error("Failed to bind data channel for session with id " + this.id, e);
            shutdownEventLoopGroup();
            return false;
        }

        // create control channel
        SocketAddress controlAddress = this.localParticipant.getControlDestination();
        try {
            ChannelFuture future = controlBootstrap.bind(controlAddress);
            this.controlChannel = future.sync().channel(); // wait for future to complete and retrieve channel

        } catch (Exception e) {
            LOG.error("Failed to bind control channel for session with id " + this.id, e);
            this.dataChannel.close();
            shutdownEventLoopGroup();
            return false;
        }

        LOG.debug("Data & Control channels bound for RtpSession with id {}.", this.id);
        // Send first RTCP packet.
        this.joinSession(this.localParticipant.getSsrc());
        this.running.set(true);

        // Add the participant database cleaner.
        this.timer.newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                if (!running.get()) {
                    return;
                }

                participantDatabase.cleanup();
                timer.newTimeout(this, participantDatabaseCleanup, TimeUnit.SECONDS);
            }
        }, this.participantDatabaseCleanup, TimeUnit.SECONDS);

        // Add the periodic RTCP report generator.
        if (this.automatedRtcpHandling) {
            this.timer.newTimeout(this, this.updatePeriodicRtcpSendInterval(), TimeUnit.SECONDS);
        }

        if (this.internalTimer) {
            this.timer.start();
        }

        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void terminate() {
        this.terminate(RtpSessionEventListener.TERMINATE_CALLED);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean sendData(byte[] data, long timestamp, boolean marked) {
        if (!this.running.get()) {
            return false;
        }

        DataPacket packet = new DataPacket();
        // Other fields will be set by sendDataPacket()
        packet.setTimestamp(timestamp);
        packet.setData(data);
        packet.setMarker(marked);

        return this.sendDataPacket(packet);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean sendDataPacket(DataPacket packet) {
        if (!this.running.get()) {
            return false;
        }
        if (!(this.payloadType == packet.getPayloadType())) {
            packet.setPayloadType(this.payloadType);
        }

        packet.setSsrc(this.localParticipant.getSsrc());
        packet.setSequenceNumber(this.sequence.incrementAndGet());
        this.internalSendData(packet);
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean sendControlPacket(ControlPacket packet) {
        // Only allow sending explicit RTCP packets if all the following conditions are met:
        // 1. session is running
        // 2. automated rtcp handling is disabled (except for APP_DATA packets) 
        if (!this.running.get()) {
            return false;
        }

        if (ControlPacket.Type.APP_DATA.equals(packet.getType()) || !this.automatedRtcpHandling) {
            this.internalSendControl(packet);
            return true;
        }

        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean sendControlPacket(CompoundControlPacket packet) {
        if (this.running.get() && !this.automatedRtcpHandling) {
            this.internalSendControl(packet);
            return true;
        }

        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public RtpParticipant getLocalParticipant() {
        return this.localParticipant;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean addReceiver(RtpParticipant remoteParticipant) {
        return (remoteParticipant.getSsrc() != this.localParticipant.getSsrc())
                && this.participantDatabase.addReceiver(remoteParticipant);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean removeReceiver(RtpParticipant remoteParticipant) {
        return this.participantDatabase.removeReceiver(remoteParticipant);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public RtpParticipant getRemoteParticipant(long ssrc) {
        return this.participantDatabase.getParticipant(ssrc);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Map<Long, RtpParticipant> getRemoteParticipants() {
        return this.participantDatabase.getMembers();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addDataListener(RtpSessionDataListener listener) {
        this.dataListeners.add(listener);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeDataListener(RtpSessionDataListener listener) {
        this.dataListeners.remove(listener);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addControlListener(RtpSessionControlListener listener) {
        this.controlListeners.add(listener);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeControlListener(RtpSessionControlListener listener) {
        this.controlListeners.remove(listener);
    }

    @Override
    public void addEventListener(RtpSessionEventListener listener) {
        this.eventListeners.add(listener);
    }

    @Override
    public void removeEventListener(RtpSessionEventListener listener) {
        this.eventListeners.remove(listener);
    }

    // DataPacketReceiver ---------------------------------------------------------------------------------------------
    /**
     * {@inheritDoc}
     */
    @Override
    public void dataPacketReceived(SocketAddress origin, DataPacket packet) {
        if (!this.running.get()) {
            return;
        }

        if (!(this.payloadType == packet.getPayloadType())) {
            // Silently discard packets of wrong payload.
            return;
        }

        // collision and loop detection:
        if (packet.getSsrc() == this.localParticipant.getSsrc()) {
            // Sending data to ourselves? Consider this a loop and bail out!
            if (origin.equals(this.localParticipant.getDataDestination())) {
                this.terminate(new Throwable("Loop detected: session is directly receiving its own packets"));
                return;
            } else if (this.collisions.incrementAndGet() > this.maxCollisionsBeforeConsideringLoop) {
                this.terminate(new Throwable("Loop detected after " + this.collisions.get() + " SSRC collisions"));
                return;
            }

            long oldSsrc = this.localParticipant.getSsrc();
            long newSsrc = this.localParticipant.resolveSsrcConflict(packet.getSsrc());

            // A collision has been detected after packets were sent, resolve by updating the local SSRC and sending
            // a BYE RTCP packet for the old SSRC.
            // http://tools.ietf.org/html/rfc3550#section-8.2
            // If no packet was sent and this is the first being received then we can avoid collisions by switching
            // our own SSRC to something else (nothing else is required because the collision was prematurely detected
            // and avoided).
            // http://tools.ietf.org/html/rfc3550#section-8.1, last paragraph
            if (this.sentOrReceivedPackets.getAndSet(true)) {
                this.leaveSession(oldSsrc, "SSRC collision detected; rejoining with new SSRC.");
                this.joinSession(newSsrc);
            }

            LOG.warn("SSRC collision with remote end detected on session with id {}; updating SSRC from {} to {}.",
                    this.id, oldSsrc, newSsrc);
            for (RtpSessionEventListener listener : this.eventListeners) {
                listener.resolvedSsrcConflict(this, oldSsrc, newSsrc);
            }
        }

        // Associate the packet with a participant or create one.
        RtpParticipant participant = this.participantDatabase.getOrCreateParticipantFromDataPacket(origin, packet);
        if (participant == null) {
            // Depending on database implementation, it may chose not to create anything, in which case this packet
            // must be discarded.
            return;
        }

        // Should the packet be discarded due to out of order SN?
        if ((participant.getLastSequenceNumber() >= packet.getSequenceNumber()) && this.discardOutOfOrder) {
            LOG.trace(
                    "Discarded out of order packet from {} in session with id {} (last SN was {}, packet SN was {}).",
                    participant, this.id, participant.getLastSequenceNumber(), packet.getSequenceNumber());
            return;
        }

        // Update last SN for participant.
        participant.setLastSequenceNumber(packet.getSequenceNumber());
        participant.setLastDataOrigin(origin);

        // Finally, dispatch the event to the data listeners.
        for (RtpSessionDataListener listener : this.dataListeners) {
            listener.dataPacketReceived(this, participant.getInfo(), packet);
        }
    }

    // ControlPacketReceiver ------------------------------------------------------------------------------------------
    /**
     * {@inheritDoc}
     */
    @Override
    public void controlPacketReceived(SocketAddress origin, CompoundControlPacket packet) {
        if (!this.running.get()) {
            return;
        }

        if (!this.automatedRtcpHandling) {
            // dispatch to the control listeners if automatedRtcpHandling is disabled
            for (RtpSessionControlListener listener : this.controlListeners) {
                listener.controlPacketReceived(this, packet);
            }
            return;
        }

        for (ControlPacket controlPacket : packet.getControlPackets()) {
            switch (controlPacket.getType()) {
            case SENDER_REPORT:
            case RECEIVER_REPORT:
                this.handleReportPacket(origin, (AbstractReportPacket) controlPacket);
                break;
            case SOURCE_DESCRIPTION:
                this.handleSdesPacket(origin, (SourceDescriptionPacket) controlPacket);
                break;
            case BYE:
                this.handleByePacket((ByePacket) controlPacket);
                break;
            case APP_DATA:
                // dispatch APP_DATA control packets to the control listeners
                // these packets are application specific 
                for (RtpSessionControlListener listener : this.controlListeners) {
                    listener.appDataReceived(this, (AppDataPacket) controlPacket);
                }
                break;
            default:
                // do nothing, unknown case
            }
        }
    }

    // Runnable from TimerTask ----------------------------------------------------------------------------------------
    /**
     * {@inheritDoc}
     * <br/>
     * Sends for each remote participant a report containing the status
     * of this session participant. 
     */
    @Override
    public void run(Timeout timeout) throws Exception {
        if (!this.running.get()) {
            return;
        }
        // send status update per remote participant
        final long currentSsrc = this.localParticipant.getSsrc();
        final SourceDescriptionPacket sdesPacket = buildSdesPacket(currentSsrc);
        this.participantDatabase.doWithReceivers(new ParticipantOperation() {
            @Override
            public void doWithParticipant(RtpParticipant participant) throws Exception {
                AbstractReportPacket report = buildReportPacket(currentSsrc, participant);
                // TODO: really to all other participants?
                // i would use:
                //                writeToControl(new CompoundControlPacket(report, sdesPacket), participant.getControlDestination());
                internalSendControl(new CompoundControlPacket(report, sdesPacket));
            }
        });

        if (!this.running.get()) {
            return;
        }
        this.timer.newTimeout(this, this.updatePeriodicRtcpSendInterval(), TimeUnit.SECONDS);
    }

    // protected helpers ----------------------------------------------------------------------------------------------
    /**
    * Shuts down the workerGroup and waits for its termination.
    */
    protected void shutdownEventLoopGroup() {
        this.workerGroup.shutdownGracefully();
        this.workerGroup.terminationFuture().syncUninterruptibly();
    }

    /**
     * <h1>automatedRtcpHandling</h1>
     * This method handles {@link SenderReportPacket}s and {@link ReceiverReportPacket}s.
     * It extracts the information of the packet and uses it to control the session.
     * 
     * @param origin origin of the packet
     * @param abstractReportPacket the report packet to handle
     */
    protected void handleReportPacket(SocketAddress origin, AbstractReportPacket abstractReportPacket) {
        if (abstractReportPacket.getReportCount() == 0) {
            return;
        }

        RtpParticipant context = this.participantDatabase.getParticipant(abstractReportPacket.getSenderSsrc());
        if (context == null) {
            // Ignore; RTCP-SDES or RTP packet must first be received.
            return;
        }

        for (ReceptionReport receptionReport : abstractReportPacket.getReports()) {
            // Ignore all reception reports except for the one who pertains to the local participant (only data that
            // matters here is the link between this participant and ourselves).
            if (receptionReport.getSsrc() == this.localParticipant.getSsrc()) {
                // TODO handle reports
            }
        }

        // For sender reports, also handle the sender information.
        if (abstractReportPacket.getType().equals(ControlPacket.Type.SENDER_REPORT)) {
            //            SenderReportPacket senderReport = (SenderReportPacket) abstractReportPacket;
            // TODO handle SenderReportPacket
        }
    }

    /**
     * This method handles the source description packets. They contain information about
     * the remote participant and therefore the objects in the {@link ParticipantDatabase} 
     * are updated if required.
     * 
     * @param origin source of the packet
     * @param packet the source description packet
     */
    protected void handleSdesPacket(SocketAddress origin, SourceDescriptionPacket packet) {
        for (SdesChunk chunk : packet.getChunks()) {
            RtpParticipant participant = this.participantDatabase.getOrCreateParticipantFromSdesChunk(origin,
                    chunk);
            if (participant == null) {
                // Depending on database implementation, it may chose not to create anything, in which case this packet
                // must be discarded.
                return;
            }
            if (!participant.hasReceivedSdes() || this.tryToUpdateOnEverySdes) {
                updateParticipant(chunk, participant);
            }
        }
    }

    /**
     * Updates the participant information with the information from the {@link SdesChunk}.
     * The {@link RtpSessionEventListener}s are informed about made changes.
     * 
     * @param chunk
     * @param participant
     */
    protected void updateParticipant(SdesChunk chunk, RtpParticipant participant) {
        participant.receivedSdes();
        // If this participant wasn't created from an SDES packet, then update its participant's description.
        if (participant.getInfo().updateFromSdesChunk(chunk)) {
            for (RtpSessionEventListener listener : this.eventListeners) {
                listener.participantInformationUpdated(this, participant);
            }
        }
    }

    /**
     * This method handles {@link ByePacket}s and therefore marks the corresponding
     * participant in the {@link ParticipantDatabase} as left.
     * 
     * @param packet
     */
    protected void handleByePacket(ByePacket packet) {
        for (Long ssrc : packet.getSsrcList()) {
            RtpParticipant participant = this.participantDatabase.getParticipant(ssrc);
            if (participant != null) {
                participant.byeReceived();
                for (RtpSessionEventListener listener : eventListeners) {
                    listener.participantLeft(this, participant);
                }
            }
        }
        LOG.trace("Received BYE for participants with SSRCs {} in session with id '{}' (reason: '{}').",
                packet.getSsrcList(), this.id, packet.getReasonForLeaving());
    }

    /**
     * Instantiates a {@link ParticipantDatabase} and returns it. This method must be implemented
     * by inheriting classes.
     * 
     * @return a reference to the reated {@link ParticipantDatabase}
     */
    protected abstract ParticipantDatabase createDatabase();

    /**
     * This method sends a {@link DataPacket} through the data channel of this session
     * to <strong>all</strong> participants.
     * @param packet the {@link DataPacket}
     */
    protected void internalSendData(final DataPacket packet) {
        this.participantDatabase.doWithReceivers(new ParticipantOperation() {
            @Override
            public void doWithParticipant(RtpParticipant participant) throws Exception {
                if (!participant.isReceiver() || participant.receivedBye()) {
                    return;
                }
                try {
                    writeToData(packet, participant.getDataDestination());
                } catch (Exception e) {
                    LOG.error("Failed to send RTP packet to participants in session with id {}.", e, id);
                }
            }

            @Override
            public String toString() {
                return "internalSendData() for session with id " + id;
            }
        });
    }

    /**
     * This method sends a {@link ControlPacket} through the control channel of this session
     * to a <strong>specific</strong> participant.
     * <br/>
     * <strong>All {@link ControlPacket}s must be sent within a {@link CompoundControlPacket}!</strong>
     * 
     * @param packet the {@link ControlPacket} to be sent
     * @param participant participant information
     */
    //    protected void internalSendControl(ControlPacket packet, RtpParticipant participant) {
    //        if (!participant.isReceiver() || participant.receivedBye()) {
    //            return;
    //        }
    //        if(!packet.getType().equals(ControlPacket.Type.RECEIVER_REPORT) 
    //              && !packet.getType().equals(ControlPacket.Type.SENDER_REPORT)) {
    //           AbstractReportPacket reportPacket = this.buildReportPacket(this.localParticipant.getSsrc(), this.localParticipant);
    //           SourceDescriptionPacket sdesPacket = this.buildSdesPacket(this.localParticipant.getSsrc());
    //            internalSendControl(new CompoundControlPacket(reportPacket, packet));
    //        }
    //        
    //
    ////        try {
    ////            this.writeToControl(packet, participant.getControlDestination());
    ////        } catch (Exception e) {
    ////            LOG.error("Failed to send RTCP packet to {} in session with id {}.", participant, this.id);
    ////        }
    //    }

    /**
     * This method sends a {@link CompoundControlPacket} through the control channel of this
     * session to a <strong>specific</strong>  participant.
     * 
     * @param packet the {@link CompoundControlPacket} to be sent
     * @param participant participant information
     */
    protected void internalSendControl(CompoundControlPacket packet, RtpParticipant participant) {
        if (!participant.isReceiver() || participant.receivedBye()) {
            return;
        }

        try {
            this.writeToControl(packet, participant.getControlDestination());
        } catch (Exception e) {
            LOG.error("Failed to send RTCP compound packet to {} in session with id {}.", e, participant, this.id);
        }
    }

    /**
     * This method sends a {@link ControlPacket} through the control channel of this session
     * to <strong>all</strong> participants.
     * <br/>
     * <strong>All {@link ControlPacket}s must be sent within a {@link CompoundControlPacket}!
     * Use with caution.</strong><br/>
     * This method is only recommended for packets of type {@link ControlPacket.Type.APP_DATA}
     * 
     * @param packet the {@link ControlPacket} to be sent
     * @param participant participant information
     */
    protected void internalSendControl(final ControlPacket packet) {
        this.participantDatabase.doWithReceivers(new SendOperation(packet));
    }

    /**
     * This method sends a {@link CompoundControlPacket} through the control channel of this
     * session to <strong>all</strong> participant.
     * 
     * @param packet the {@link CompoundControlPacket} to be sent
     * @param participant participant information
     */
    protected void internalSendControl(final CompoundControlPacket packet) {
        this.participantDatabase.doWithReceivers(new SendOperation(packet));
    }

    /**
     * Writes the packets information to the data channel
     * 
     * @param packet
     * @param destination
     */
    protected void writeToData(DataPacket packet, SocketAddress destination) {
        final AddressedEnvelope<DataPacket, SocketAddress> envelope = new DefaultAddressedEnvelope<>(packet,
                destination);
        this.dataChannel.writeAndFlush(envelope);
    }

    /**
     * Write the packets information to the control channel
     * 
     * @param packet
     * @param destination
     */
    protected void writeToControl(ControlPacket packet, SocketAddress destination) {
        // FIXME: does not work currently -> add new encoder for ControlPackets wrapped into Envelopes
        final AddressedEnvelope<ControlPacket, SocketAddress> envelope = new DefaultAddressedEnvelope<>(packet,
                destination);
        this.controlChannel.writeAndFlush(envelope);
    }

    /**
     * Write the packets information to the control channel
     * 
     * @param packet
     * @param destination
     */
    protected void writeToControl(CompoundControlPacket packet, SocketAddress destination) {
        final AddressedEnvelope<CompoundControlPacket, SocketAddress> envelope = new DefaultAddressedEnvelope<>(
                packet, destination);
        this.controlChannel.writeAndFlush(envelope);
    }

    /**
     * Joins the current session with the given SSRC by sending an empty receiver
     * report packet. This only works if {@code automatedRtcpHandling} is turned on.
     * 
     * @param currentSsrc SSRC of this (local) participant
     */
    protected void joinSession(final long currentSsrc) {
        if (!this.automatedRtcpHandling) {
            return;
        }
        // Joining a session, so send an empty receiver report.
        final ReceiverReportPacket emptyReceiverReport = new ReceiverReportPacket();
        emptyReceiverReport.setSenderSsrc(currentSsrc);
        // Send also an SDES packet in the compound RTCP packet.
        final SourceDescriptionPacket sdesPacket = this.buildSdesPacket(currentSsrc);

        this.internalSendControl(new CompoundControlPacket(emptyReceiverReport, sdesPacket));
    }

    /**
     * Leaves the current session by sending a bye packet.
     * 
     * @param currentSsrc 
     * @param motive reason for leaving the session
     */
    protected void leaveSession(final long currentSsrc, final String motive) {
        if (!this.automatedRtcpHandling) {
            return;
        }

        final SourceDescriptionPacket sdesPacket = this.buildSdesPacket(currentSsrc);
        final ByePacket byePacket = new ByePacket();
        byePacket.addSsrc(currentSsrc);
        byePacket.setReasonForLeaving(motive);

        this.internalSendControl(new CompoundControlPacket(sdesPacket, byePacket));
    }

    /**
     * Creates a new report packet. <br/>
     * If no packets were sent with this session before, a {@link ReceiverReportPacket} is
     * created, otherwise it is a {@link SenderReportPacket}.
     * 
     * @param currentSsrc this (local) participants SSRC
     * @param context receiver of this report packet
     * @return If no packets were sent with this session before, a {@link ReceiverReportPacket} is 
     * returned, otherwise it is a {@link SenderReportPacket}.
     */
    protected AbstractReportPacket buildReportPacket(long currentSsrc, RtpParticipant context) {
        AbstractReportPacket packet;
        if (this.getSentPackets() == 0) {
            // If no packets were sent to this source, then send a receiver report.
            packet = new ReceiverReportPacket();
        } else {
            // Otherwise, build a sender report.
            SenderReportPacket senderPacket = new SenderReportPacket();
            senderPacket.setNtpTimestamp(new BigInteger("0")); // FIXME
            senderPacket.setRtpTimestamp(System.currentTimeMillis()); // FIXME
            senderPacket.setSenderPacketCount(this.getSentPackets());
            senderPacket.setSenderOctetCount(this.getSentBytes());
            packet = senderPacket;
        }
        packet.setSenderSsrc(currentSsrc);

        // If this source sent data, then calculate the link quality to build a reception report block.
        if (context.getReceivedPackets() > 0) {
            ReceptionReport block = new ReceptionReport();
            block.setSsrc(context.getInfo().getSsrc());
            block.setDelaySinceLastSenderReport(0); // FIXME
            block.setFractionLost((short) 0); // FIXME
            block.setExtendedHighestSequenceNumberReceived(0); // FIXME
            block.setInterArrivalJitter(0); // FIXME
            block.setCumulativeNumberOfPacketsLost(0); // FIXME
            packet.addReportBlock(block);
        }

        return packet;
    }

    /**
     * Extracts the source description information from {@link #localParticipant} to
     * generate a {@link SourceDescriptionPacket}. The SSRC is set manually to prevent
     * lightheaded mistakes.
     * 
     * @param currentSsrc
     * @return a {@link SourceDescriptionPacket}
     */
    protected SourceDescriptionPacket buildSdesPacket(long currentSsrc) {
        SourceDescriptionPacket sdesPacket = new SourceDescriptionPacket();
        SdesChunk chunk = new SdesChunk(currentSsrc);

        RtpParticipantInfo info = this.localParticipant.getInfo();
        if (info.getCname() == null) {
            info.setCname(new StringBuilder().append("efflux/").append(this.id).append('@')
                    .append(this.dataChannel.localAddress()).toString());
        }
        chunk.addItem(SdesChunkItems.createCnameItem(info.getCname()));

        if (info.getName() != null) {
            chunk.addItem(SdesChunkItems.createNameItem(info.getName()));
        }

        if (info.getEmail() != null) {
            chunk.addItem(SdesChunkItems.createEmailItem(info.getEmail()));
        }

        if (info.getPhone() != null) {
            chunk.addItem(SdesChunkItems.createPhoneItem(info.getPhone()));
        }

        if (info.getLocation() != null) {
            chunk.addItem(SdesChunkItems.createLocationItem(info.getLocation()));
        }

        if (info.getTool() == null) {
            info.setTool(VERSION);
        }
        chunk.addItem(SdesChunkItems.createToolItem(info.getTool()));

        if (info.getNote() != null) {
            chunk.addItem(SdesChunkItems.createLocationItem(info.getNote()));
        }
        sdesPacket.addItem(chunk);

        return sdesPacket;
    }

    /**
     * Stops this session by stopping all timers, leaving the session and closing 
     * all closables to release all used resources.
     * 
     * @param cause
     */
    protected synchronized void terminate(Throwable cause) {
        // Always set to false, even if it was already set to false.
        if (!this.running.getAndSet(false)) {
            return;
        }

        if (this.internalTimer) {
            this.timer.stop();
        }

        this.dataListeners.clear();
        this.controlListeners.clear();

        // Close data channel, send BYE RTCP packets and close control channel.
        this.dataChannel.close();
        this.leaveSession(this.localParticipant.getSsrc(), "Session terminated, because: " + cause.toString());
        this.controlChannel.close();

        for (RtpSessionEventListener listener : this.eventListeners) {
            listener.sessionTerminated(this, cause);
        }
        this.eventListeners.clear();
        shutdownEventLoopGroup();
        LOG.debug("RtpSession with id {} terminated. Cause: {}", this.id, cause);
    }

    protected void resetSendStats() {
        this.sentByteCounter.set(0);
        this.sentPacketCounter.set(0);
    }

    protected long incrementSentBytes(int delta) {
        if (delta < 0) {
            return this.sentByteCounter.get();
        }

        return this.sentByteCounter.addAndGet(delta);
    }

    protected long incrementSentPackets() {
        return this.sentPacketCounter.incrementAndGet();
    }

    protected long updatePeriodicRtcpSendInterval() {
        // TODO make this adaptative
        this.periodicRtcpSendInterval = 5;
        return this.periodicRtcpSendInterval;
    }

    // getters & setters ----------------------------------------------------------------------------------------------
    @Override
    public boolean isRunning() {
        return this.running.get();
    }

    @Override
    public boolean useNio() {
        return useNio;
    }

    /**
     * Can only be modified before initialization.
     */
    @Override
    public void setUseNio(boolean useNio) {
        if (this.running.get()) {
            throw new IllegalArgumentException("Cannot modify property after initialisation");
        }
        this.useNio = useNio;
    }

    public boolean isDiscardOutOfOrder() {
        return discardOutOfOrder;
    }

    /**
     * Can only be modified before initialization.
     */
    public void setDiscardOutOfOrder(boolean discardOutOfOrder) {
        if (this.running.get()) {
            throw new IllegalArgumentException("Cannot modify property after initialisation");
        }
        this.discardOutOfOrder = discardOutOfOrder;
    }

    public int getBandwidthLimit() {
        return bandwidthLimit;
    }

    /**
     * Can only be modified before initialization.
     */
    public void setBandwidthLimit(int bandwidthLimit) {
        if (this.running.get()) {
            throw new IllegalArgumentException("Cannot modify property after initialisation");
        }
        this.bandwidthLimit = bandwidthLimit;
    }

    public int getSendBufferSize() {
        return sendBufferSize;
    }

    /**
     * Can only be modified before initialization.
     */
    public void setSendBufferSize(int sendBufferSize) {
        if (this.running.get()) {
            throw new IllegalArgumentException("Cannot modify property after initialisation");
        }
        this.sendBufferSize = sendBufferSize;
    }

    public int getReceiveBufferSize() {
        return receiveBufferSize;
    }

    /**
     * Can only be modified before initialization.
     */
    public void setReceiveBufferSize(int receiveBufferSize) {
        if (this.running.get()) {
            throw new IllegalArgumentException("Cannot modify property after initialisation");
        }
        this.receiveBufferSize = receiveBufferSize;
    }

    public int getMaxCollisionsBeforeConsideringLoop() {
        return maxCollisionsBeforeConsideringLoop;
    }

    /**
     * Can only be modified before initialization.
     */
    public void setMaxCollisionsBeforeConsideringLoop(int maxCollisionsBeforeConsideringLoop) {
        if (this.running.get()) {
            throw new IllegalArgumentException("Cannot modify property after initialisation");
        }
        this.maxCollisionsBeforeConsideringLoop = maxCollisionsBeforeConsideringLoop;
    }

    public boolean isAutomatedRtcpHandling() {
        return automatedRtcpHandling;
    }

    /**
     * Can only be modified before initialization.
     */
    public void setAutomatedRtcpHandling(boolean automatedRtcpHandling) {
        if (this.running.get()) {
            throw new IllegalArgumentException("Cannot modify property after initialisation");
        }
        this.automatedRtcpHandling = automatedRtcpHandling;
    }

    public boolean isTryToUpdateOnEverySdes() {
        return tryToUpdateOnEverySdes;
    }

    /**
     * Can only be modified before initialization.
     */
    public void setTryToUpdateOnEverySdes(boolean tryToUpdateOnEverySdes) {
        if (this.running.get()) {
            throw new IllegalArgumentException("Cannot modify property after initialisation");
        }
        this.tryToUpdateOnEverySdes = tryToUpdateOnEverySdes;
    }

    public long getSentBytes() {
        return this.sentByteCounter.get();
    }

    public long getSentPackets() {
        return this.sentPacketCounter.get();
    }

    public int getParticipantDatabaseCleanup() {
        return participantDatabaseCleanup;
    }

    /**
     * Can only be modified before initialization.
     */
    public void setParticipantDatabaseCleanup(int participantDatabaseCleanup) {
        if (this.running.get()) {
            throw new IllegalArgumentException("Cannot modify property after initialisation");
        }
        this.participantDatabaseCleanup = participantDatabaseCleanup;
    }

    // private classes ------------------------------------------------------------------------------------------------
    /**
     * Implementation of the {@link ParticipantOperation} interface for sending a packet to the participant.
     * 
     * @author <a href="https://github.com/CodeLionX">CodeLionX</a>
     */
    private class SendOperation implements ParticipantOperation {

        private final boolean isCompound;
        private ControlPacket packet;
        private CompoundControlPacket compoundPacket;

        /**
         * This class sends a {@link ControlPacket} to the participant.
         * @param packet
         */
        public SendOperation(final ControlPacket packet) {
            this.packet = packet;
            this.isCompound = false;
        }

        /**
         * This class sends a {@link CompoundControlPacket} to the participant.
         * @param packet
         */
        public SendOperation(final CompoundControlPacket packet) {
            this.compoundPacket = packet;
            this.isCompound = true;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void doWithParticipant(RtpParticipant participant) throws Exception {
            if (!participant.isReceiver() || participant.receivedBye()) {
                return;
            }
            try {
                if (isCompound) {
                    writeToControl(compoundPacket, participant.getControlDestination());
                } else {
                    writeToControl(packet, participant.getControlDestination());
                }
            } catch (Exception e) {
                LOG.error("Failed to send RTCP packet to participants in session with id {}.", e, id);
            }
        }

        @Override
        public String toString() {
            return "internalSendControl(" + (isCompound ? "CompoundControlPacket" : "ControlPacket")
                    + ") for session with id " + id;
        }
    }
}