/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
*
* This program 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 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton 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 BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.voiceconf.red5.media;
import java.io.IOException;
import java.net.DatagramSocket;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.slf4j.Logger;
import org.bigbluebutton.voiceconf.red5.media.net.RtpPacket;
import org.bigbluebutton.voiceconf.red5.media.net.RtpSocket;
import org.red5.logging.Red5LoggerFactory;
public class RtpStreamReceiver {
protected static Logger log = Red5LoggerFactory.getLogger(RtpStreamReceiver.class, "sip");
// Maximum blocking time, spent waiting for reading new bytes [milliseconds]
// private static final int SO_TIMEOUT = 200;
private static int RTP_HEADER_SIZE = 12;
private RtpSocket rtpSocket = null;
private final Executor exec = Executors.newSingleThreadExecutor();
private Runnable rtpPacketReceiver;
private volatile boolean receivePackets = false;
private RtpStreamReceiverListener listener;
private final int payloadLength;
private int lastSequenceNumber = 0;
private long lastPacketTimestamp = 0;
private boolean firstPacket = true;
private boolean lastPacketDropped = false;
private int successivePacketDroppedCount = 0;
private long lastPacketReceived = 0;
public RtpStreamReceiver(DatagramSocket socket, int expectedPayloadLength) {
this.payloadLength = expectedPayloadLength;
rtpSocket = new RtpSocket(socket);
initializeSocket();
}
public void setRtpStreamReceiverListener(RtpStreamReceiverListener listener) {
this.listener = listener;
}
private void initializeSocket() {
}
public void start() {
receivePackets = true;
rtpPacketReceiver = new Runnable() {
public void run() {
receiveRtpPackets();
}
};
exec.execute(rtpPacketReceiver);
}
public void stop() {
receivePackets = false;
}
public void receiveRtpPackets() {
int packetReceivedCounter = 0;
int internalBufferLength = payloadLength + RTP_HEADER_SIZE;
byte[] internalBuffer = new byte[internalBufferLength];
RtpPacket rtpPacket = new RtpPacket(internalBuffer, internalBufferLength);;
while (receivePackets) {
try {
rtpSocket.receive(rtpPacket);
packetReceivedCounter++;
if (shouldDropDelayedPacket(rtpPacket)) {
continue;
}
if (rtpPacket.isRtcpPacket()) {
/**
* Asterisk (1.6.2.5) send RTCP packets. We just ignore them (for now).
* It could be for KeepAlive (http://tools.ietf.org/html/draft-ietf-avt-app-rtp-keepalive-09)
*/
if (log.isDebugEnabled())
log.debug("RTCP packet [" + rtpPacket.getRtcpPayloadType() + ", length=" + rtpPacket.getPayloadLength() + "] seqNum[rtpSeqNum=" + rtpPacket.getSeqNum() + ",lastSeqNum=" + lastSequenceNumber
+ "][rtpTS=" + rtpPacket.getTimestamp() + ",lastTS=" + lastPacketTimestamp + "][port=" + rtpSocket.getDatagramSocket().getLocalPort() + "]");
} else {
if (shouldHandlePacket(rtpPacket)) {
lastSequenceNumber = rtpPacket.getSeqNum();
lastPacketTimestamp = rtpPacket.getTimestamp();
processRtpPacket(internalBuffer, RTP_HEADER_SIZE, rtpPacket.getPayloadLength());
} else {
if (log.isDebugEnabled())
log.debug("Corrupt packet [" + rtpPacket.getRtcpPayloadType() + "," + rtpPacket.getPayloadType() + ", length=" + rtpPacket.getPayloadLength() + "] seqNum[rtpSeqNum=" + rtpPacket.getSeqNum() + ",lastSeqNum=" + lastSequenceNumber
+ "][rtpTS=" + rtpPacket.getTimestamp() + ",lastTS=" + lastPacketTimestamp + "][port=" + rtpSocket.getDatagramSocket().getLocalPort() + "]");
if (lastPacketDropped) successivePacketDroppedCount++;
else lastPacketDropped = true;
}
}
} catch (IOException e) { // We get this when the socket closes when the call hangs up.
receivePackets = false;
}
}
log.debug("Rtp Receiver stopped. Packet Received = " + packetReceivedCounter + "." );
}
private boolean shouldDropDelayedPacket(RtpPacket rtpPacket) {
long now = System.currentTimeMillis();
if (now - lastPacketReceived > 200) {
if (log.isDebugEnabled())
log.debug("Delayed packet [" + rtpPacket.getRtcpPayloadType() + "," + rtpPacket.getPayloadType() + ", length=" + rtpPacket.getPayloadLength() + "] seqNum[rtpSeqNum=" + rtpPacket.getSeqNum() + ",lastSeqNum=" + lastSequenceNumber
+ "][rtpTS=" + rtpPacket.getTimestamp() + ",lastTS=" + lastPacketTimestamp + "][port=" + rtpSocket.getDatagramSocket().getLocalPort() + "]");
lastPacketReceived = now;
return true;
}
lastPacketReceived = now;
return false;
}
private boolean isMarkerPacket(RtpPacket rtpPacket) {
/*
* FreeSWITCH sends a marker packet at the beginning of the voice frame.
* If you stop talking and then start talking, a marker packet is received on start talking. (ralam sept 20, 2010).
*/
if (rtpPacket.hasMarker()) {
if (log.isDebugEnabled())
log.debug("Marked packet [" + rtpPacket.getPayloadType() + ", length=" + rtpPacket.getPayloadLength() + "] seqNum[rtpSeqNum=" + rtpPacket.getSeqNum() + ",lastSeqNum=" + lastSequenceNumber
+ "][rtpTS=" + rtpPacket.getTimestamp() + ",lastTS=" + lastPacketTimestamp + "][port=" + rtpSocket.getDatagramSocket().getLocalPort() + "]");
return true;
}
return false;
}
private boolean shouldHandlePacket(RtpPacket rtpPacket) {
/** Take seq number only into account and not timestamps. Seems like the timestamp sometimes change whenever the audio changes source.
* For example, in FreeSWITCH, the audio prompt will have it's own "start" timestamp and then
* another "start" timestamp will be generated for the voice. (ralam, sept 7, 2010).
* && packetIsNotCorrupt(rtpPacket)) {
**/
return isFirstPacket(rtpPacket) || isMarkerPacket(rtpPacket) || resetDueToSuccessiveDroppedPackets() || validSeqNum(rtpPacket) || seqNumRolledOver(rtpPacket);
}
private boolean resetDueToSuccessiveDroppedPackets() {
/*
* I notice that Asterisk (1.6.2.5) sets the rtp sequence number to 12 every time it sends a marked rtp packet. This screws up our
* way of determining which packet to drop. To get around this, we detect if consecutive packets have been dropped then reset
* the sequence number to handle the next incoming packets (ralam sept. 20, 2010).
*/
if (lastPacketDropped && successivePacketDroppedCount > 3) {
if (log.isDebugEnabled())
log.debug("Resetting after successive dropped packets [successivePacketDroppedCount=" + successivePacketDroppedCount +
"][port=" + rtpSocket.getDatagramSocket().getLocalPort() + "]");
lastPacketDropped = false;
successivePacketDroppedCount = 0;
return true;
}
return false;
}
private boolean isFirstPacket(RtpPacket rtpPacket) {
if (firstPacket) {
lastPacketReceived = System.currentTimeMillis();
firstPacket = false;
if (log.isDebugEnabled())
log.debug("First packet [" + rtpPacket.getPayloadType() + ", length=" + rtpPacket.getPayloadLength() + "] seqNum[rtpSeqNum=" + rtpPacket.getSeqNum() + ",lastSeqNum=" + lastSequenceNumber
+ "][rtpTS=" + rtpPacket.getTimestamp() + ",lastTS=" + lastPacketTimestamp + "][port=" + rtpSocket.getDatagramSocket().getLocalPort() + "]");
return true;
}
return false;
}
private boolean validSeqNum(RtpPacket rtpPacket) {
/*
* Assume if the sequence number jumps by more that 100, that the sequence number is corrupt.
*/
return (rtpPacket.getSeqNum() > lastSequenceNumber && rtpPacket.getSeqNum() - lastSequenceNumber < 100);
}
private boolean seqNumRolledOver(RtpPacket rtpPacket) {
/*
* Max sequence num is 65535 (16-bits). Let's use 65000 as check to take into account
* delayed packets.
*/
if (lastSequenceNumber - rtpPacket.getSeqNum() > 65000) {
if (log.isDebugEnabled())
log.debug("Packet rolling over seqNum[rtpSeqNum=" + rtpPacket.getSeqNum() + ",lastSeqNum=" + lastSequenceNumber
+ "][rtpTS=" + rtpPacket.getTimestamp() + ",lastTS=" + lastPacketTimestamp + "][port=" + rtpSocket.getDatagramSocket().getLocalPort() + "]");
return true;
}
return false;
}
private void processRtpPacket(byte[] rtpAudio, int offset, int len) {
if (listener != null) listener.onAudioDataReceived(rtpAudio, offset, len);
else log.debug("No listener for incoming audio packet");
}
}