/* * 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.rtp; import java.io.IOException; import java.net.PortUnreachableException; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; import org.apache.log4j.Logger; import org.restcomm.media.rtp.rfc2833.DtmfOutput; import org.restcomm.media.rtp.secure.DtlsHandler; import org.restcomm.media.rtp.statistics.RtpStatistics; import org.restcomm.media.scheduler.PriorityQueueScheduler; import org.restcomm.media.sdp.format.AVProfile; import org.restcomm.media.sdp.format.RTPFormat; import org.restcomm.media.sdp.format.RTPFormats; import org.restcomm.media.spi.memory.Frame; /** * Transmits RTP packets over a channel. * * @author Oifa Yulian * @author Henrique Rosa (henrique.rosa@telestax.com) * */ public class RtpTransmitter { private static final Logger LOGGER = Logger.getLogger(RtpTransmitter.class); // Channel properties private DatagramChannel channel; private final RtpClock rtpClock; private final RtpStatistics statistics; private boolean dtmfSupported; private final RTPOutput rtpOutput; private final DtmfOutput dtmfOutput; // Packet representations with internal buffers private final RtpPacket rtpPacket = new RtpPacket(RtpPacket.RTP_PACKET_MAX_SIZE, true); private final RtpPacket oobPacket = new RtpPacket(RtpPacket.RTP_PACKET_MAX_SIZE, true); // WebRTC private DtlsHandler dtlsHandler; private boolean secure; // Details of a transmitted packet private RTPFormats formats; private RTPFormat currentFormat; private long timestamp; private long dtmfTimestamp; private long dtmfDuration; private int sequenceNumber; public RtpTransmitter(final PriorityQueueScheduler scheduler, final RtpClock clock, final RtpStatistics statistics) { this.rtpClock = clock; this.statistics = statistics; this.dtmfSupported = false; this.rtpOutput = new RTPOutput(scheduler, this); this.dtmfOutput = new DtmfOutput(scheduler, this); this.sequenceNumber = 0; this.dtmfTimestamp = -1; this.dtmfDuration = -1; this.timestamp = -1; this.formats = null; this.secure = false; } public void setFormatMap(final RTPFormats rtpFormats) { this.dtmfSupported = rtpFormats.contains(AVProfile.telephoneEventsID); this.formats = rtpFormats; } public RTPOutput getRtpOutput() { return rtpOutput; } public DtmfOutput getDtmfOutput() { return dtmfOutput; } public void enableSrtp(final DtlsHandler handler) { this.secure = true; this.dtlsHandler = handler; } public void disableSrtp() { this.secure = false; this.dtlsHandler = null; } public void activate() { this.rtpOutput.activate(); this.dtmfOutput.activate(); } public void deactivate() { this.rtpOutput.deactivate(); this.dtmfOutput.deactivate(); this.dtmfSupported = false; } public void setChannel(final DatagramChannel channel) { this.channel = channel; } private boolean isConnected() { return this.channel != null && this.channel.isConnected(); } private void disconnect() throws IOException { if(this.channel != null) { this.channel.disconnect(); } } public void reset() { deactivate(); clear(); } public void clear() { this.timestamp = -1; this.dtmfTimestamp = -1; this.dtmfDuration = -1; // Reset format in case connection is reused. // Otherwise it would point to incorrect codec. this.currentFormat = null; } private void send(RtpPacket packet) throws IOException { // Do not send data while DTLS handshake is ongoing. WebRTC calls only. if(this.secure && !this.dtlsHandler.isHandshakeComplete()) { return; } // Secure RTP packet. WebRTC calls only. // SRTP handler returns null if an error occurs ByteBuffer buffer = packet.getBuffer(); if (this.secure) { byte[] rtpData = new byte[buffer.limit()]; buffer.get(rtpData, 0, rtpData.length); byte[] srtpData = this.dtlsHandler.encodeRTP(rtpData, 0, rtpData.length); if(srtpData == null || srtpData.length == 0) { LOGGER.warn("Could not secure RTP packet! Packet dropped."); return; } else { buffer.clear(); buffer.put(srtpData); buffer.flip(); } } if(packet != null) { channel.send(buffer, channel.socket().getRemoteSocketAddress()); // send RTP packet to the network and update statistics for RTCP statistics.onRtpSent(packet); } } public void sendDtmf(Frame frame) { if (!this.dtmfSupported) { frame.recycle(); return; } // ignore frames with duplicate timestamp if (frame.getTimestamp() / 1000000L == dtmfTimestamp) { frame.recycle(); return; } // // convert to milliseconds first // dtmfTimestamp = frame.getTimestamp() / 1000000L; // // convert to rtp time units // dtmfTimestamp = rtpClock.convertToRtpTime(dtmfTimestamp); // oobPacket.wrap(false, AVProfile.telephoneEventsID, this.sequenceNumber++, dtmfTimestamp, this.statistics.getSsrc(), // frame.getData(), frame.getOffset(), frame.getLength()); // hrosa - Hack to workaround MEDIA-61: https://telestax.atlassian.net/browse/MEDIA-61 long duration = (frame.getData()[2]<<8) | (frame.getData()[3] & 0xFF); boolean toneChanged = false; if(this.dtmfDuration == -1 || this.dtmfDuration > duration) { this.dtmfTimestamp = this.timestamp; toneChanged = true; } this.dtmfDuration = duration; oobPacket.wrap(toneChanged, AVProfile.telephoneEventsID, this.sequenceNumber++, this.dtmfTimestamp, this.statistics.getSsrc(), frame.getData(), frame.getOffset(), frame.getLength()); // end of hack - hrosa frame.recycle(); try { if(isConnected()) { send(oobPacket); } } catch (PortUnreachableException e) { try { // icmp unreachable received // disconnect and wait for new packet disconnect(); } catch (IOException ex) { LOGGER.error(ex.getMessage(), ex); } } catch (Exception e) { LOGGER.error(e.getMessage(), e); } } public void send(Frame frame) { // discard frame if format is unknown if (frame.getFormat() == null) { frame.recycle(); return; } // determine current RTP format if it is unknown if (currentFormat == null || !currentFormat.getFormat().matches(frame.getFormat())) { currentFormat = formats.getRTPFormat(frame.getFormat()); // discard packet if format is still unknown if (currentFormat == null) { frame.recycle(); return; } // update clock rate rtpClock.setClockRate(currentFormat.getClockRate()); } // ignore frames with duplicate timestamp if (frame.getTimestamp() / 1000000L == timestamp) { frame.recycle(); return; } // convert to milliseconds first timestamp = frame.getTimestamp() / 1000000L; // convert to rtp time units timestamp = rtpClock.convertToRtpTime(timestamp); rtpPacket.wrap(false, currentFormat.getID(), this.sequenceNumber++, timestamp, this.statistics.getSsrc(), frame.getData(), frame.getOffset(), frame.getLength()); frame.recycle(); try { if (isConnected()) { send(rtpPacket); } } catch (PortUnreachableException e) { // icmp unreachable received // disconnect and wait for new packet try { disconnect(); } catch (IOException ex) { LOGGER.error(ex.getMessage(), ex); } } catch (Exception e) { LOGGER.error(e.getMessage(), e); } } }