/* * TeleStax, Open Source Cloud Communications * Copyright 2011-2014, Telestax Inc and individual contributors * by the @authors tag. * * This program is free software: you can redistribute it and/or modify * under the terms of the GNU Affero General Public License as * published by the Free Software Foundation; either version 3 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/> * */ package org.restcomm.media.rtcp; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.log4j.Logger; import org.restcomm.media.network.deprecated.channel.PacketHandler; import org.restcomm.media.network.deprecated.channel.PacketHandlerException; import org.restcomm.media.rtp.RtpPacket; import org.restcomm.media.rtp.secure.DtlsHandler; import org.restcomm.media.rtp.statistics.RtpStatistics; import org.restcomm.media.scheduler.Scheduler; /** * * @author Henrique Rosa (henrique.rosa@telestax.com) * */ public class RtcpHandler implements PacketHandler { private static final Logger logger = Logger.getLogger(RtcpHandler.class); /** Time (in ms) between SSRC Task executions */ private static final long SSRC_TASK_DELAY = 7000; /* Core elements */ private DatagramChannel channel; private ByteBuffer byteBuffer; private int pipelinePriority; /* Scheduler */ private final Scheduler scheduler; private TxTask scheduledTask; private Future<?> reportTaskFuture; private final SsrcTask ssrcTask; private Future<?> ssrcTaskFuture; /* RTCP elements */ private final RtpStatistics statistics; /** The elapsed time (milliseconds) since an RTCP packet was transmitted */ private long tp; /** The time interval (milliseconds) until next scheduled transmission time of an RTCP packet */ private long tn; /** Flag that is true if the application has not yet sent an RTCP packet */ private AtomicBoolean initial; /** Flag that is true once the handler joined an RTP session */ private AtomicBoolean joined; /* WebRTC */ /** Checks whether communication of this channel is secure. WebRTC calls only. */ private boolean secure; /** Handles the DTLS handshake and encodes/decodes secured packets. For WebRTC calls only. */ private DtlsHandler dtlsHandler; public RtcpHandler(final Scheduler scheduler, final RtpStatistics statistics) { // Scheduler this.scheduler = scheduler; this.ssrcTask = new SsrcTask(); // core stuff this.pipelinePriority = 0; this.byteBuffer = ByteBuffer.allocateDirect(RtpPacket.RTP_PACKET_MAX_SIZE); // rtcp stuff this.statistics = statistics; this.scheduledTask = null; this.tp = 0; this.tn = -1; this.initial = new AtomicBoolean(true); this.joined = new AtomicBoolean(false); // webrtc this.secure = false; this.dtlsHandler = null; } @Override public int getPipelinePriority() { return pipelinePriority; } public void setPipelinePriority(int pipelinePriority) { this.pipelinePriority = pipelinePriority; } /** * Gets the time interval between the current time and another time stamp. * * @param timestamp The time stamp, in milliseconds, to compare to the current time * @return The interval of time between both time stamps, in milliseconds. */ private long resolveInterval(long timestamp) { long interval = timestamp - this.statistics.getCurrentTime(); return (interval < 0) ? 0 : interval; } public void setChannel(DatagramChannel channel) { this.channel = channel; } /** * Gets whether the handler is in initial stage.<br> * The handler is in initial stage until it has sent at least one RTCP packet during the current RTP session. * * @return true if not rtcp packet has been sent, false otherwise. */ public boolean isInitial() { return initial.get(); } /** * Gets whether the handler is currently joined to an RTP Session. * * @return Return true if joined. Otherwise, returns false. */ public boolean isJoined() { return joined.get(); } /** * Upon joining the session, the participant initializes tp to 0, tc to 0, senders to 0, pmembers to 1, members to 1, * we_sent to false, rtcp_bw to the specified fraction of the session bandwidth, initial to true, and avg_rtcp_size to the * probable size of the first RTCP packet that the application will later construct. * * The calculated interval T is then computed, and the first packet is scheduled for time tn = T. This means that a * transmission timer is set which expires at time T. Note that an application MAY use any desired approach for implementing * this timer. * * The participant adds its own SSRC to the member table. */ public void joinRtpSession() { if (!this.joined.get()) { // Schedule first RTCP packet long t = this.statistics.rtcpInterval(this.initial.get()); this.tn = this.statistics.getCurrentTime() + t; scheduleRtcp(this.tn, RtcpPacketType.RTCP_REPORT); // Start SSRC timeout timer this.ssrcTaskFuture = this.scheduler.scheduleWithFixedDelay(ssrcTask, SSRC_TASK_DELAY, SSRC_TASK_DELAY, TimeUnit.MILLISECONDS); this.joined.set(true); } } public void leaveRtpSession() { if (this.joined.get()) { this.joined.set(false); /* * When the participant decides to leave the system, tp is reset to tc, the current time, members and pmembers are * initialized to 1, initial is set to 1, we_sent is set to false, senders is set to 0, and avg_rtcp_size is set to * the size of the compound BYE packet. * * The calculated interval T is computed. The BYE packet is then scheduled for time tn = tc + T. */ this.tp = this.statistics.getCurrentTime(); this.statistics.resetMembers(); this.initial.set(true); this.statistics.clearSenders(); // XXX Sending the BYE packet NOW, since channel will be closed - hrosa // long t = this.statistics.rtcpInterval(initial); // this.tn = resolveDelay(t); // this.scheduleRtcp(this.tn, RtcpPacketType.RTCP_BYE); // cancel scheduled task and schedule BYE now if(this.reportTaskFuture != null) { this.reportTaskFuture.cancel(true); } // Send BYE // Do not run in separate thread so channel can be properly closed by the owner of this handler this.statistics.setRtcpPacketType(RtcpPacketType.RTCP_BYE); this.scheduledTask = new TxTask(RtcpPacketType.RTCP_BYE); this.scheduledTask.run(); } } /** * Gets the time interval until the next report is sent. * * @return Returns the time interval in milliseconds until the report is sent. Returns -1 if no report is currently * scheduled. */ public long getNextScheduledReport() { long delay = this.tn - statistics.getCurrentTime(); return delay < 0 ? -1 : delay; } /** * Schedules an event to occur at a certain time. * * @param timestamp The time (in milliseconds) when the event should be fired * @param packet The RTCP packet to be sent when the timer expires */ private void scheduleRtcp(long timestamp, RtcpPacketType packetType) { // Create the task and schedule it long interval = resolveInterval(timestamp); this.scheduledTask = new TxTask(packetType); try { this.reportTaskFuture = this.scheduler.schedule(this.scheduledTask, interval, TimeUnit.MILLISECONDS); // Let the RTP handler know what is the type of scheduled packet this.statistics.setRtcpPacketType(packetType); } catch (IllegalStateException e) { logger.warn("RTCP timer already canceled. No more reports will be scheduled."); } } private void scheduleNow(RtcpPacketType packetType) { this.scheduledTask = new TxTask(packetType); try { this.reportTaskFuture = this.scheduler.submit(this.scheduledTask); // Let the RTP handler know what is the type of scheduled packet this.statistics.setRtcpPacketType(packetType); } catch (IllegalStateException e) { logger.warn("RTCP timer already canceled. No more reports will be scheduled."); } } /** * Re-schedules a previously scheduled event. * * @param timestamp The time stamp (in milliseconds) of the rescheduled event */ private void rescheduleRtcp(TxTask task, long timestamp) { // Cancel current execution of the task this.reportTaskFuture.cancel(true); // Re-schedule task execution long interval = resolveInterval(timestamp); try { this.reportTaskFuture = this.scheduler.schedule(task, interval, TimeUnit.MILLISECONDS); } catch (IllegalStateException e) { logger.warn("RTCP timer already canceled. Scheduled report was canceled and cannot be re-scheduled."); } } /** * Secures the channel, meaning all traffic is SRTCP. * * SRTCP handlers will only be available to process traffic after a DTLS handshake is completed. * * @param remotePeerFingerprint The DTLS fingerprint of the remote peer. Use to setup DTLS keying material. */ public void enableSRTCP(DtlsHandler dtlsHandler) { this.dtlsHandler = dtlsHandler; this.secure = true; } /** * Disables secure layer on the channel, meaning all traffic is treated as plain RTCP. */ public void disableSRTCP() { this.dtlsHandler = null; this.secure = false; } @Override public boolean canHandle(byte[] packet) { return canHandle(packet, packet.length, 0); } @Override public boolean canHandle(byte[] packet, int dataLength, int offset) { byte b0 = packet[offset]; int b0Int = b0 & 0xff; // Differentiate between RTP, STUN and DTLS packets in the pipeline // https://tools.ietf.org/html/rfc5764#section-5.1.2 if (b0Int > 127 && b0Int < 192) { // RTP version field must equal 2 int version = (b0 & 0xC0) >> 6; if (version == RtpPacket.VERSION) { // The payload type field of the first RTCP packet in a compound // packet must be equal to SR or RR. int type = packet[offset + 1] & 0x000000FF; if (type == RtcpHeader.RTCP_SR || type == RtcpHeader.RTCP_RR) { /* * The padding bit (P) should be zero for the first packet of a compound RTCP packet because padding should * only be applied, if it is needed, to the last packet. */ int padding = (packet[offset] & 0x20) >> 5; if (padding == 0) { /* * TODO The length fields of the individual RTCP packets must add up to the overall length of the * compound RTCP packet as received. This is a fairly strong check. */ return true; } } } } return false; } @Override public byte[] handle(byte[] packet, InetSocketAddress localPeer, InetSocketAddress remotePeer) throws PacketHandlerException { return handle(packet, packet.length, 0, localPeer, remotePeer); } @Override public byte[] handle(byte[] packet, int dataLength, int offset, InetSocketAddress localPeer, InetSocketAddress remotePeer) throws PacketHandlerException { // Do NOT handle data if have not joined RTP session if(!this.joined.get()) { return null; } // Do NOT handle data while DTLS handshake is ongoing. WebRTC calls only. if (this.secure && !this.dtlsHandler.isHandshakeComplete()) { return null; } // Check if incoming packet is supported by the handler if (!canHandle(packet, dataLength, offset)) { logger.warn("Cannot handle incoming packet!"); throw new PacketHandlerException("Cannot handle incoming packet"); } // Decode the RTCP compound packet RtcpPacket rtcpPacket = new RtcpPacket(); if (this.secure) { byte[] decoded = this.dtlsHandler.decodeRTCP(packet, offset, dataLength); if (decoded == null || decoded.length == 0) { logger.warn("Could not decode incoming SRTCP packet. Packet will be dropped."); return null; } rtcpPacket.decode(decoded, 0); } else { rtcpPacket.decode(packet, offset); } // Trace incoming RTCP report if (logger.isDebugEnabled()) { logger.debug("\nRECEIVED " + rtcpPacket.toString()); } // Upgrade RTCP statistics this.statistics.onRtcpReceive(rtcpPacket); if (RtcpPacketType.RTCP_BYE.equals(rtcpPacket.getPacketType())) { if (RtcpPacketType.RTCP_REPORT.equals(this.scheduledTask.getPacketType())) { /* * To make the transmission rate of RTCP packets more adaptive to changes in group membership, the following * "reverse reconsideration" algorithm SHOULD be executed when a BYE packet is received that reduces members to * a value less than pmembers */ if (this.statistics.getMembers() < this.statistics.getPmembers()) { long tc = this.statistics.getCurrentTime(); this.tn = tc + (this.statistics.getMembers() / this.statistics.getPmembers()) * (this.tn - tc); this.tp = tc - (this.statistics.getMembers() / this.statistics.getPmembers()) * (tc - this.tp); // Reschedule the next report for time tn rescheduleRtcp(this.scheduledTask, this.tn); this.statistics.confirmMembers(); } } } // RTCP handler does not send replies return null; } private void sendRtcpPacket(RtcpPacket packet) throws IOException { // Do NOT attempt to send packet if have not joined RTP session if(this.joined.get()) { return; } // DO NOT attempt to send packet while DTLS handshake is ongoing if (this.secure && !this.dtlsHandler.isHandshakeComplete()) { return; } RtcpPacketType type = packet.hasBye() ? RtcpPacketType.RTCP_BYE : RtcpPacketType.RTCP_REPORT; if (this.channel != null && channel.isOpen() && channel.isConnected()) { // decode packet byte[] data = new byte[RtpPacket.RTP_PACKET_MAX_SIZE]; packet.encode(data, 0); int dataLength = packet.getSize(); // If channel is secure, convert RTCP packet to SRTCP. WebRTC calls only. if (this.secure) { data = this.dtlsHandler.encodeRTCP(data, 0, dataLength); dataLength = data.length; } // prepare buffer byteBuffer.clear(); byteBuffer.rewind(); byteBuffer.put(data, 0, dataLength); byteBuffer.flip(); byteBuffer.rewind(); // trace outgoing RTCP report if (logger.isDebugEnabled()) { logger.debug("\nSENDING " + packet.toString()); } // Make double sure channel is still open and connected before sending if (channel.isOpen() && channel.isConnected()) { // send packet // XXX Should register on RTP statistics IF sending fails! this.channel.send(this.byteBuffer, this.channel.getRemoteAddress()); } else { // cancel packet transmission if (logger.isDebugEnabled()) { logger.debug("Could not send " + type + " packet because channel is closed or disconnected."); } return; } // If we send at least one RTCP packet then initial = false this.initial.set(false); // update RTCP statistics this.statistics.onRtcpSent(packet); } else { if (logger.isDebugEnabled()) { logger.debug("Could not send " + type + " packet because channel is closed or disconnected."); } } } public synchronized void reset() { if (joined.get()) { throw new IllegalStateException("Cannot reset handler while is part of active RTP session."); } if (this.reportTaskFuture != null) { this.reportTaskFuture.cancel(false); this.reportTaskFuture = null; this.scheduledTask = null; } if (this.ssrcTaskFuture != null) { this.ssrcTaskFuture.cancel(false); this.ssrcTaskFuture = null; } this.tp = 0; this.tn = -1; this.initial.set(true); this.joined.set(false); if (this.secure) { disableSRTCP(); } } /** * Disconnects and closes the datagram channel used to send and receive RTCP traffic. */ private void closeChannel() { if (this.channel != null) { if (this.channel.isConnected()) { try { this.channel.disconnect(); } catch (IOException e) { logger.warn(e.getMessage(), e); } } if (this.channel.isOpen()) { try { this.channel.close(); } catch (IOException e) { logger.warn(e.getMessage(), e); } } } } public int compareTo(PacketHandler o) { if (o == null) { return 1; } return this.getPipelinePriority() - o.getPipelinePriority(); } /** * Runnable task responsible for sending RTCP packets. */ private class TxTask implements Runnable { private final RtcpPacketType packetType; public TxTask(RtcpPacketType packetType) { this.packetType = packetType; } public RtcpPacketType getPacketType() { return this.packetType; } @Override public void run() { try { onExpire(); } catch (IOException e) { logger.error("Cannot send scheduled RTCP report. Stopping handler."); reset(); } } /** * This function is responsible for deciding whether to send an RTCP report or BYE packet now, or to reschedule * transmission. * * It is also responsible for updating the pmembers, initial, tp, and avg_rtcp_size state variables. This function * should be called upon expiration of the event timer used by Schedule(). * * @param task The scheduled task whose timer expired * * @throws IOException When a packet cannot be sent over the datagram channel */ private void onExpire() throws IOException { long t; long tc = statistics.getCurrentTime(); switch (this.packetType) { case RTCP_REPORT: if (joined.get()) { t = statistics.rtcpInterval(RtcpHandler.this.initial.get()); RtcpHandler.this.tn = RtcpHandler.this.tp + t; if (tn <= tc) { // Send currently scheduled packet and update statistics RtcpPacket report = RtcpPacketFactory.buildReport(statistics); sendRtcpPacket(report); tp = tc; /* * We must redraw the interval. Don't reuse the one computed above, since its not actually * distributed the same, as we are conditioned on it being small enough to cause a packet to be * sent. */ t = statistics.rtcpInterval(initial.get()); tn = tc + t; } // schedule next packet (only if still in RTP session) scheduleRtcp(tn, RtcpPacketType.RTCP_REPORT); statistics.confirmMembers(); } break; case RTCP_BYE: /* * In the case of a BYE, we use "timer reconsideration" to reschedule the transmission of the BYE if * necessary */ // XXX decided to send RTCP BYE right away - hrosa // t = statistics.rtcpInterval(initial); t = 0; tn = tp + t; // Send BYE and stop scheduling further packets RtcpPacket bye = RtcpPacketFactory.buildBye(statistics); // Set the avg_packet_size to the size of the compound BYE packet statistics.setRtcpAvgSize(bye.getSize()); // Send the BYE and close channel sendRtcpPacket(bye); break; default: logger.warn("Unkown scheduled event type!"); break; } } } /** * Runnable task responsible for checking timeouts of registered SSRC. */ private class SsrcTask implements Runnable { @Override public void run() { statistics.isSenderTimeout(); } } }