/*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.restcomm.media.rtp;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.PortUnreachableException;
import java.net.SocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import org.apache.log4j.Logger;
import org.restcomm.media.component.audio.AudioComponent;
import org.restcomm.media.component.oob.OOBComponent;
import org.restcomm.media.network.deprecated.ProtocolHandler;
import org.restcomm.media.network.deprecated.UdpManager;
import org.restcomm.media.rtp.crypto.DtlsSrtpServerProvider;
import org.restcomm.media.rtp.jitter.FixedJitterBuffer;
import org.restcomm.media.rtp.rfc2833.DtmfInput;
import org.restcomm.media.rtp.rfc2833.DtmfOutput;
import org.restcomm.media.rtp.secure.DtlsHandler;
import org.restcomm.media.scheduler.PriorityQueueScheduler;
import org.restcomm.media.scheduler.Task;
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.ConnectionMode;
import org.restcomm.media.spi.FormatNotSupportedException;
import org.restcomm.media.spi.dsp.Processor;
import org.restcomm.media.spi.format.AudioFormat;
import org.restcomm.media.spi.format.FormatFactory;
import org.restcomm.media.spi.format.Formats;
import org.restcomm.media.spi.memory.Frame;
import org.restcomm.media.spi.utils.Text;
/**
*
* @author Oifa Yulian
* @author Henrique Rosa
*
* @deprecated use {@link RtpChannel}
*/
@Deprecated
public class RTPDataChannel {
private Logger logger = Logger.getLogger(RTPDataChannel.class);
private final static int PORT_ANY = -1;
private final static AudioFormat LINEAR_FORMAT = FormatFactory.createAudioFormat("LINEAR", 8000, 16, 1);
private final static AudioFormat DTMF_FORMAT = FormatFactory.createAudioFormat("telephone-event", 8000);
static {
DTMF_FORMAT.setOptions(new Text("0-15"));
}
private final long ssrc = System.currentTimeMillis();
// Available Channels
private ChannelsManager channelsManager;
private DatagramChannel rtpChannel;
private DatagramChannel rtcpChannel;
private boolean rtpChannelBound = false;
private boolean rtcpChannelBound = false;
// Receiver and transmitter
private RTPInput input;
private RTPOutput output;
// RTP dtmf receiver and trasmitter
private DtmfInput dtmfInput;
private DtmfOutput dtmfOutput;
// tx task - sender
private TxTask tx = new TxTask();
// RTP clock
private RtpClock rtpClock, oobClock;
// allowed jitter
private int jitterBufferSize;
// Media stream format
private RTPFormats rtpFormats = new RTPFormats();
// Remote peer address
private SocketAddress remotePeer;
private int sn;
private int count;
private RTPHandler rtpHandler;
private volatile long rxCount;
private volatile long txCount;
private FixedJitterBuffer rxBuffer;
private Formats formats = new Formats();
private Boolean shouldReceive = false;
private Boolean shouldLoop = false;
private HeartBeat heartBeat;
private long lastPacketReceived;
private RTPChannelListener rtpChannelListener;
private PriorityQueueScheduler scheduler;
private UdpManager udpManager;
private AudioComponent audioComponent;
private OOBComponent oobComponent;
private boolean sendDtmf = false;
// WebRTC
private boolean isWebRtc = false;
private DtlsHandler webRtcHandler;
private DtlsSrtpServerProvider dtlsServerProvider;
/**
* Create RTP channel instance.
*
* @param channelManager
* Channel manager
*
*/
protected RTPDataChannel(ChannelsManager channelsManager, int channelId, DtlsSrtpServerProvider dtlsServerProvider) {
this.channelsManager = channelsManager;
this.jitterBufferSize = channelsManager.getJitterBufferSize();
// open data channel
rtpHandler = new RTPHandler();
// create clock with RTP units
rtpClock = new RtpClock(channelsManager.getClock());
oobClock = new RtpClock(channelsManager.getClock());
rxBuffer = new FixedJitterBuffer(rtpClock, jitterBufferSize);
scheduler = channelsManager.getScheduler();
udpManager = channelsManager.getUdpManager();
// receiver
input = new RTPInput(scheduler, rxBuffer);
rxBuffer.setListener(input);
// transmittor
output = new RTPOutput(scheduler, this);
dtmfInput = new DtmfInput(scheduler, oobClock);
dtmfOutput = new DtmfOutput(scheduler, this);
heartBeat = new HeartBeat();
formats.add(LINEAR_FORMAT);
audioComponent = new AudioComponent(channelId);
audioComponent.addInput(input.getAudioInput());
audioComponent.addOutput(output.getAudioOutput());
oobComponent = new OOBComponent(channelId);
oobComponent.addInput(dtmfInput.getOOBInput());
oobComponent.addOutput(dtmfOutput.getOOBOutput());
this.dtlsServerProvider = dtlsServerProvider;
}
public AudioComponent getAudioComponent() {
return this.audioComponent;
}
public OOBComponent getOOBComponent() {
return this.oobComponent;
}
public void setInputDsp(Processor dsp) {
input.setDsp(dsp);
}
public Processor getInputDsp() {
return input.getDsp();
}
public void setOutputDsp(Processor dsp) {
output.setDsp(dsp);
}
public Processor getOutputDsp() {
return output.getDsp();
}
public void setOutputFormats(Formats fmts)
throws FormatNotSupportedException {
output.setFormats(fmts);
}
public void setRtpChannelListener(RTPChannelListener rtpChannelListener) {
this.rtpChannelListener = rtpChannelListener;
}
public void updateMode(ConnectionMode connectionMode) {
switch (connectionMode) {
case SEND_ONLY:
shouldReceive = false;
shouldLoop = false;
audioComponent.updateMode(false, true);
oobComponent.updateMode(false, true);
dtmfInput.deactivate();
input.deactivate();
output.activate();
dtmfOutput.activate();
break;
case RECV_ONLY:
shouldReceive = true;
shouldLoop = false;
audioComponent.updateMode(true, false);
oobComponent.updateMode(true, false);
dtmfInput.activate();
input.activate();
output.deactivate();
dtmfOutput.deactivate();
break;
case INACTIVE:
shouldReceive = false;
shouldLoop = false;
audioComponent.updateMode(false, false);
oobComponent.updateMode(false, false);
dtmfInput.deactivate();
input.deactivate();
output.deactivate();
dtmfOutput.deactivate();
break;
case SEND_RECV:
case CONFERENCE:
shouldReceive = true;
shouldLoop = false;
audioComponent.updateMode(true, true);
oobComponent.updateMode(true, true);
dtmfInput.activate();
input.activate();
output.activate();
dtmfOutput.activate();
break;
case NETWORK_LOOPBACK:
shouldReceive = false;
shouldLoop = true;
audioComponent.updateMode(false, false);
oobComponent.updateMode(false, false);
dtmfInput.deactivate();
input.deactivate();
output.deactivate();
dtmfOutput.deactivate();
break;
default:
break;
}
boolean connectImmediately = false;
if (this.remotePeer != null)
connectImmediately = udpManager
.connectImmediately((InetSocketAddress) this.remotePeer);
if (udpManager.getRtpTimeout() > 0 && this.remotePeer != null
&& !connectImmediately) {
if (shouldReceive) {
lastPacketReceived = scheduler.getClock().getTime();
scheduler.submitHeatbeat(heartBeat);
} else {
heartBeat.cancel();
}
}
}
/**
* Binds channel to the first available port.
*
* @throws SocketException
*/
public void bind(boolean isLocal) throws IOException, SocketException {
try {
rtpChannel = udpManager.open(rtpHandler);
// if control enabled open rtcp channel as well
if (channelsManager.getIsControlEnabled()) {
rtcpChannel = udpManager.open(new RTCPHandler());
}
} catch (IOException e) {
throw new SocketException(e.getMessage());
}
// bind data channel
if (!isLocal) {
this.rxBuffer.setInUse(true);
udpManager.bind(rtpChannel, PORT_ANY);
} else {
this.rxBuffer.setInUse(false);
udpManager.bindLocal(rtpChannel, PORT_ANY);
}
this.rtpChannelBound = true;
// if control enabled open rtcp channel as well
if (channelsManager.getIsControlEnabled()) {
if (!isLocal)
udpManager.bind(rtcpChannel, rtpChannel.socket()
.getLocalPort() + 1);
else
udpManager.bindLocal(rtcpChannel, rtpChannel.socket()
.getLocalPort() + 1);
}
}
public void bind(DatagramChannel channel) throws IOException {
this.rxBuffer.setInUse(true);
this.rtpChannel = channel;
if (this.isWebRtc) {
this.webRtcHandler.setChannel(this.rtpChannel);
}
this.udpManager.open(this.rtpChannel, this.rtpHandler);
this.rtpChannelBound = true;
}
public boolean isDataChannelBound() {
return rtpChannelBound;
}
/**
* Gets the port number to which this channel is bound.
*
* @return the port number.
*/
public int getLocalPort() {
return rtpChannel != null ? rtpChannel.socket().getLocalPort() : 0;
}
/**
* Sets the address of remote peer.
*
* @param address
* the address object.
*/
public void setPeer(SocketAddress address) {
this.remotePeer = address;
boolean connectImmediately = false;
if (rtpChannel != null) {
if (rtpChannel.isConnected())
try {
rtpChannel.disconnect();
} catch (IOException e) {
logger.error(e);
}
connectImmediately = udpManager
.connectImmediately((InetSocketAddress) address);
if (connectImmediately)
try {
rtpChannel.connect(address);
} catch (IOException e) {
logger.info("Can not connect to remote address , please check that you are not using local address - 127.0.0.X to connect to remote");
logger.error(e);
}
}
if (udpManager.getRtpTimeout() > 0 && !connectImmediately) {
if (shouldReceive) {
lastPacketReceived = scheduler.getClock().getTime();
scheduler.submitHeatbeat(heartBeat);
} else {
heartBeat.cancel();
}
}
}
/**
* Closes this socket.
*/
public void close() {
if (rtpChannel != null) {
if (rtpChannel.isConnected()) {
try {
rtpChannel.disconnect();
} catch (IOException e) {
logger.error(e);
}
try {
rtpChannel.socket().close();
rtpChannel.close();
} catch (IOException e) {
logger.error(e);
}
}
}
if (rtcpChannel != null) {
rtcpChannel.socket().close();
}
// System.out.println("RX COUNT:" + rxCount + ",TX COUNT:" + txCount);
rxCount = 0;
txCount = 0;
input.deactivate();
dtmfInput.deactivate();
dtmfInput.reset();
output.deactivate();
dtmfOutput.deactivate();
this.tx.clear();
heartBeat.cancel();
sendDtmf = false;
}
public int getPacketsLost() {
return input.getPacketsLost();
}
public long getPacketsReceived() {
return rxCount;
}
public long getPacketsTransmitted() {
return txCount;
}
/**
* Modifies the map between format and RTP payload number
*
* @param rtpFormats
* the format map
*/
public void setFormatMap(RTPFormats rtpFormats) {
if (rtpFormats.find(AVProfile.telephoneEventsID) != null)
sendDtmf = true;
else
sendDtmf = false;
this.rtpHandler.flush();
this.rtpFormats = rtpFormats;
}
protected void send(Frame frame) {
///XXX WebRTC hack - dataChannel only available after ICE negotiation!
if (rtpChannel != null && rtpChannel.isConnected())
tx.perform(frame);
}
public void sendDtmf(Frame frame) {
if (rtpChannel.isConnected())
tx.performDtmf(frame);
}
/**
* Checks whether the data channel is available for media exchange.
*
* @return
*/
public boolean isAvailable() {
// The channel is available is is connected
boolean available = this.rtpChannel != null && this.rtpChannel.isConnected();
// In case of WebRTC calls the DTLS handshake must be completed
if(this.isWebRtc) {
available = available && this.webRtcHandler.isHandshakeComplete();
}
return available;
}
/**
* Implements IO operations for RTP protocol.
*
* This class is attached to channel and when channel is ready for IO the
* scheduler will call either receive or send.
*/
private class RTPHandler implements ProtocolHandler {
// The schedulable task for read operation
private RxTask rx = new RxTask();
private volatile boolean isReading = false;
private SelectionKey selectionKey;
/**
* (Non Java-doc.)
*
* @see org.restcomm.media.network.deprecated.ProtocolHandler#receive(java.nio.channels.DatagramChannel)
*/
public void receive(DatagramChannel channel) {
RTPDataChannel.this.count++;
rx.perform();
}
public boolean isReadable() {
return !this.isReading;
}
public boolean isWriteable() {
return true;
}
private void flush() {
if (rtpChannelBound) {
rx.flush();
}
}
public void onClosed() {
if (rtpChannelListener != null)
rtpChannelListener.onRtpFailure();
}
/**
* (Non Java-doc.)
*
* @see org.restcomm.media.network.deprecated.ProtocolHandler#send(java.nio.channels.DatagramChannel)
*/
public void send(DatagramChannel channel) {
}
public void setKey(SelectionKey key) {
this.selectionKey = key;
}
}
/**
* Implements IO operations for RTCP protocol.
*
*/
private class RTCPHandler implements ProtocolHandler {
public void receive(DatagramChannel channel) {
throw new UnsupportedOperationException("Not supported yet.");
}
public void send(DatagramChannel channel) {
throw new UnsupportedOperationException("Not supported yet.");
}
public void setKey(SelectionKey key) {
throw new UnsupportedOperationException("Not supported yet.");
}
public boolean isReadable() {
throw new UnsupportedOperationException("Not supported yet.");
}
public boolean isWriteable() {
throw new UnsupportedOperationException("Not supported yet.");
}
public void onClosed() {
}
}
/**
* Implements scheduled rx job.
*
*/
private class RxTask {
// RTP packet representation
private RtpPacket rtpPacket = new RtpPacket(RtpPacket.RTP_PACKET_MAX_SIZE, true);
private RTPFormat format;
private SocketAddress currAddress;
private RxTask() {
super();
}
private void flush() {
SocketAddress currAddress;
try {
// lets clear the receiver
currAddress = rtpChannel.receive(rtpPacket.getBuffer());
rtpPacket.getBuffer().clear();
while (currAddress != null) {
currAddress = rtpChannel.receive(rtpPacket.getBuffer());
rtpPacket.getBuffer().clear();
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
/**
* (Non Java-doc.)
*
* @see org.restcomm.media.scheduler.Task#perform()
*/
public long perform() {
// Make sure the DTLS is completed for WebRTC calls
if (isWebRtc && !webRtcHandler.isHandshakeComplete()) {
// Handshake is performed on a different thread
// So its necessary to check if handshake is ongoing
if(!webRtcHandler.isHandshaking()) {
webRtcHandler.handshake();
}
// Avoid blocking the scheduler
// A future poll task will take care of RTP transmission once handshake is complete
} else {
perform2();
}
return 0;
}
private void perform2() {
try {
currAddress=null;
try {
currAddress = receiveRtpPacket(rtpPacket);
if (currAddress != null && !rtpChannel.isConnected()) {
rxBuffer.restart();
rtpChannel.connect(currAddress);
} else if (currAddress != null && rxCount == 0) {
rxBuffer.restart();
}
} catch(PortUnreachableException e) {
try {
// ICMP unreachable received.
// Disconnect and wait for new packet.
rtpChannel.disconnect();
}
catch(IOException ex) {
logger.error(ex.getMessage(), ex);
}
}
catch (IOException e) {
logger.error(e.getMessage(), e);
}
while (currAddress != null) {
lastPacketReceived = scheduler.getClock().getTime();
if (rtpPacket.getVersion() != 0 && (shouldReceive || shouldLoop)) {
// RTP v0 packets is used in some application.
// Discarding since we do not handle them
// Queue packet into the receiver jitter buffer
if (rtpPacket.getBuffer().limit() > 0) {
if (shouldLoop && rtpChannel.isConnected()) {
sendRtpPacket(rtpPacket);
rxCount++;
txCount++;
} else if (!shouldLoop) {
format = rtpFormats.find(rtpPacket.getPayloadType());
if (format != null && format.getFormat().matches(DTMF_FORMAT)) {
dtmfInput.write(rtpPacket);
} else {
rxBuffer.write(rtpPacket, format);
}
rxCount++;
}
}
}
currAddress = receiveRtpPacket(rtpPacket);
}
}
catch(PortUnreachableException e) {
// ICMP unreachable received
// Disconnect and wait for new packet
try {
rtpChannel.disconnect();
} catch(IOException ex) {
logger.error(ex.getMessage(), ex);
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
rtpHandler.isReading = false;
}
}
/**
* Writer job.
*/
private class TxTask {
private RtpPacket rtpPacket = new RtpPacket(
RtpPacket.RTP_PACKET_MAX_SIZE, true);
private RtpPacket oobPacket = new RtpPacket(
RtpPacket.RTP_PACKET_MAX_SIZE, true);
private RTPFormat fmt;
private long timestamp = -1;
private long dtmfTimestamp = -1;
private TxTask() {
}
/**
* if connection is reused fmt could point to old codec , which in case
* will be incorrect
*
*/
public void clear() {
this.timestamp = -1;
this.dtmfTimestamp = -1;
this.fmt = null;
}
public void performDtmf(Frame frame) {
if (!sendDtmf) {
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, sn++,
dtmfTimestamp, ssrc, frame.getData(), frame.getOffset(),
frame.getLength());
frame.recycle();
try {
if (rtpChannel.isConnected()) {
sendRtpPacket(oobPacket);
txCount++;
}
} catch (PortUnreachableException e) {
// icmp unreachable received
// disconnect and wait for new packet
try {
rtpChannel.disconnect();
} catch (IOException ex) {
logger.error(ex);
}
} catch (Exception e) {
logger.error(e);
}
}
public void perform(Frame frame) {
// discard frame if format is unknown
if (frame.getFormat() == null) {
frame.recycle();
return;
}
// if current rtp format is unknown determine it
if (fmt == null || !fmt.getFormat().matches(frame.getFormat())) {
fmt = rtpFormats.getRTPFormat(frame.getFormat());
// format still unknown? discard packet
if (fmt == null) {
frame.recycle();
return;
}
// update clock rate
rtpClock.setClockRate(fmt.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, fmt.getID(), sn++, timestamp, ssrc,
frame.getData(), frame.getOffset(), frame.getLength());
frame.recycle();
try {
if (rtpChannel.isConnected()) {
sendRtpPacket(rtpPacket);
txCount++;
}
} catch (PortUnreachableException e) {
// icmp unreachable received
// disconnect and wait for new packet
try {
rtpChannel.disconnect();
} catch (IOException ex) {
logger.error(ex);
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
private class HeartBeat extends Task {
public HeartBeat() {
super();
}
public int getQueueNumber() {
return PriorityQueueScheduler.HEARTBEAT_QUEUE;
}
@Override
public long perform() {
if (scheduler.getClock().getTime() - lastPacketReceived > udpManager.getRtpTimeout() * 1000000000L) {
if (rtpChannelListener != null)
rtpChannelListener.onRtpFailure();
} else {
scheduler.submitHeatbeat(this);
}
return 0;
}
}
/**
* Enables WebRTC encryption for the RTP channel.
*
* @param remotePeerFingerprint
*/
public void enableWebRTC(Text remotePeerFingerprint) {
this.isWebRtc = true;
if (this.webRtcHandler == null) {
this.webRtcHandler = new DtlsHandler(this.dtlsServerProvider);
}
this.webRtcHandler.setRemoteFingerprint("sha-256", remotePeerFingerprint.toString());
}
public Text getWebRtcLocalFingerprint() {
if(this.webRtcHandler != null) {
return new Text(this.webRtcHandler.getLocalFingerprint());
}
return new Text();
}
private SocketAddress receiveRtpPacket(RtpPacket packet) throws IOException {
SocketAddress address = null;
if (this.isWebRtc) {
// XXX not used anymore
// this.webRtcHandler.decodeRTP(packet);
}
// WebRTC handler can return null if packet is not valid
if(packet != null) {
// Clear the buffer for a fresh read
ByteBuffer buf = packet.getBuffer();
buf.clear();
// receive RTP packet from the network
address = rtpChannel.receive(buf);
// put the pointer at the beginning of the buffer
buf.flip();
}
return address;
}
private void sendRtpPacket(RtpPacket packet) throws IOException {
// Do not send data while DTLS handshake is ongoing. WebRTC calls only.
if(isWebRtc && !this.webRtcHandler.isHandshakeComplete()) {
return;
}
// Secure RTP packet. WebRTC calls only.
if (isWebRtc) {
// XXX not used anymore
// this.webRtcHandler.encodeRTP(packet);
}
// SRTP handler returns null if an error occurs
if(packet != null) {
// Rewind buffer
ByteBuffer buf = packet.getBuffer();
buf.rewind();
// send RTP packet to the network
rtpChannel.send(buf, rtpChannel.socket().getRemoteSocketAddress());
}
}
public String getExternalAddress() {
return this.udpManager.getExternalAddress();
}
}