/*
* Copyright 2007 Sun Microsystems, Inc.
*
* This file is part of jVoiceBridge.
*
* jVoiceBridge is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation and distributed hereunder
* to you.
*
* jVoiceBridge 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Sun designates this particular file as subject to the "Classpath"
* exception as provided by Sun in the License file that accompanied this
* code.
*/
package com.sun.voip.server;
import com.sun.voip.AudioConversion;
import com.sun.voip.CallParticipant;
import com.sun.voip.CallEvent;
import com.sun.voip.DataUpdater;
import com.sun.voip.JitterManager;
import com.sun.voip.JitterObject;
import com.sun.voip.Logger;
import com.sun.voip.MediaInfo;
import com.sun.voip.MixDataSource;
import com.sun.voip.Recorder;
import com.sun.voip.RtcpReceiver;
import com.sun.voip.RtpPacket;
import com.sun.voip.RtpSocket;
import com.sun.voip.RtpReceiverPacket;
import com.sun.voip.SampleRateConverter;
import com.sun.voip.SdpManager;
import com.sun.voip.SpeechDetector;
import com.sun.voip.SpeexDecoder;
import com.sun.voip.SpeexException;
import com.sun.voip.TreatmentDoneListener;
import com.sun.voip.TreatmentManager;
import com.sun.voip.Util;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.channels.DatagramChannel;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.ArrayList;
import java.util.NoSuchElementException;
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.text.ParseException;
import java.awt.Point;
import org.ifsoft.*;
import org.ifsoft.rtp.*;
import org.jitsi.impl.neomedia.codec.audio.opus.Opus;
import org.xmpp.jnodes.IChannel;
/**
* Receive RTP data for this ConferenceMember, add it to the mix
* and keep statistics.
*/
public class MemberReceiver implements MixDataSource, TreatmentDoneListener {
private ConferenceManager conferenceManager;
private ConferenceMember member;
private CallHandler callHandler;
private CallParticipant cp; // caller parameters
private boolean traceCall = false;
private boolean isAutoMuted; // to suppress dtmf sounds
/*
* Each member can only be whispering in one group at a time.
*/
private WhisperGroup whisperGroup; // currently whispering in this group
private WhisperGroup conferenceWhisperGroup;
private byte telephoneEventPayload;
private MediaInfo myMediaInfo;
private double inputVolume = 1.0;
private boolean readyToReceiveData = false;
private boolean gotComfortPayload = false; // flag COMFORT_PAYLOAD
private byte comfortNoiseLevel; // comfort noise level
private SpeechDetector speechDetector = null;
private DtmfDecoder dtmfDecoder = null;
private int dtmfPackets;
private static boolean forwardDtmfKeys = true;
private RtpReceiverPacket packet;
private SpeexDecoder speexDecoder;
private long opusDecoder = 0;
private final int opusSampleRate = 48000;
private final int frameSizeInMillis = 20;
private final int outputFrameSize = 2;
private final int opusChannels = 2;
private int frameSizeInSamplesPerChannel = (opusSampleRate * frameSizeInMillis) / 1000;
private int frameSizeInBytes = outputFrameSize * opusChannels * frameSizeInSamplesPerChannel;
private int dropPackets;
private boolean done = false;
private int myMemberNumber;
private static int memberNumber;
private static Object memberNumberLock = new Object();
/*
* Statistics
*/
private String timeStarted;
private int packetsReceived = 0;
private int packetsDropped = 0;
private int comfortPayloadsReceived = 0;
private int comfortPayloadsSent = 0;
private long timeCurrentPacketReceived;
private long timePreviousPacketReceived;
private long totalTime = 0;
private long timeToProcessMediaPackets;
private int mediaPacketsReceived;
private int lastMediaPacketsReceived;
private int badPayloads;
private long previousRtpTimestamp = 0;
private boolean joinConfirmationReceived = false;
private static String joinConfirmationKey = "1";
private Cipher decryptCipher;
private String encryptionKey;
private String encryptionAlgorithm;
private SampleRateConverter inSampleRateConverter;
private DatagramChannel datagramChannel;
private SelectionKey selectionKey;
private RtcpReceiver rtcpReceiver;
private JitterManager jitterManager;
private ArrayList<MemberSender> forwardMemberList = new ArrayList<MemberSender>();
private boolean initializationDone = false;
private IChannel relayChannel = null;
public void setChannel(IChannel relayChannel)
{
this.relayChannel = relayChannel;
}
public MemberReceiver(ConferenceMember member, CallParticipant cp, DatagramChannel datagramChannel) throws IOException
{
this.member = member;
this.cp = cp;
this.datagramChannel = datagramChannel;
synchronized (memberNumberLock) {
myMemberNumber = memberNumber++;
}
encryptionKey = cp.getEncryptionKey();
encryptionAlgorithm = cp.getEncryptionAlgorithm();
if (encryptionKey != null) {
try {
if (encryptionKey.length() < 8) {
encryptionKey +=
String.valueOf(System.currentTimeMillis());
}
if (encryptionKey.length() > 8 &&
encryptionAlgorithm.equals("DES")) {
encryptionKey = encryptionKey.substring(0, 8);
}
byte[] keyBytes = encryptionKey.getBytes();
SecretKeySpec secretKey = new SecretKeySpec(keyBytes,
encryptionAlgorithm);
decryptCipher = Cipher.getInstance(encryptionAlgorithm);
decryptCipher.init(Cipher.DECRYPT_MODE, secretKey);
Logger.println("Call " + cp + " Voice data will be decrypted "
+ "using " + encryptionAlgorithm);
} catch (Exception e) {
Logger.println("Call " + cp
+ " Crypto initialization failed " + e.getMessage());
throw new IOException(" Crypto initialization failed "
+ e.getMessage());
}
}
timeStarted = Logger.getDate();
}
public ConferenceMember getMember()
{
return member;
}
public MediaInfo getMediaInfo()
{
return myMediaInfo;
}
public byte getTelephoneEventPayload()
{
return telephoneEventPayload;
}
/*
* For debugging.
*/
public void traceCall(boolean traceCall)
{
this.traceCall = traceCall;
}
public boolean traceCall()
{
return traceCall;
}
public String getPerformanceData() throws IOException {
if (done) {
throw new IOException("Call " + cp + " has ended");
}
String s = "PacketsReceived=" + packetsReceived;
s += ":MissingPackets=" + packet.getOutOfSequencePackets();
s += ":JitterBufferSize=" + jitterManager.getJitterBufferSize();
return s;
}
public void setCnThresh(int cnThresh) {
if (speechDetector == null) {
Logger.println("Can't set cnThresh because there is no "
+ "speech detector");
return;
}
speechDetector.setCnThresh(cnThresh);
}
public void setPowerThresholdLimit(float powerThresholdLimit) {
if (speechDetector == null) {
Logger.println("Can't set powerThresholdLimit because there is no "
+ "speech detector");
return;
}
speechDetector.setPowerThresholdLimit(powerThresholdLimit);
}
public String getMemberState() {
if (initializationDone == false) {
return "";
}
String s = "";
if (callHandler != null) {
s += "\tBridge receive address for data from call "
+ callHandler.getReceiveAddress() + "\n";
}
s += "\tJoinConfirmationReceived " + joinConfirmationReceived + "\n";
s += "\tTelephone Event Payload " + telephoneEventPayload + "\n";
s += "\tIsAutoMuted " + isAutoMuted + "\n";
s += "\tIsMuted " + cp.isMuted() + "\n";
s += "\tIsConferenceMuted " + cp.isConferenceMuted() + "\n";
s += "\tIsConferenceSilenced " + cp.isConferenceSilenced() + "\n";
s += "\tReadyToReceiveData " + readyToReceiveData() + "\n";
s += "\tInput volume ";
s += inputVolume + " ";
s += "\n";
s += "\tSeconds since last Rtcp report "
+ rtcpReceiver.secondsSinceLastReport(member.getRtcpAddress()) + "\n";
s += "\tMilliseconds since last packet received "
+ (System.currentTimeMillis() - timeCurrentPacketReceived + "\n");
s += "\tMedia packets received " + mediaPacketsReceived + "\n";
s += "\tMin jitter size " + jitterManager.getMinJitterBufferSize()
+ " packets\n";
s += "\tMax jitter size " + jitterManager.getMaxJitterBufferSize()
+ " packets\n";
s += "\tJitter Buffer size " + jitterManager.getJitterBufferSize()
+ "\n";
s += "\tPacketLossConcealment class name "
+ jitterManager.getPlcClassName() + "\n";
s += "\tWhispering in " + whisperGroup.toAbbreviatedString() + "\n";
s += "\tComfort Payload Received " + gotComfortPayload + "\n";
s += "\tForced to defer mixing " + forcedToDeferMixing + "\n";
synchronized (forwardMemberList) {
if (forwardMemberList.size() > 0) {
s += "\tForwarding data to\n";
for (MemberSender memberSender : forwardMemberList) {
s += "\t\t" + memberSender + "\n";
}
}
}
return s;
}
/**
* Initialize this member. The call has been established and
* we now know the port at which the member (CallParticipant)
* listens for data.
*/
public void initialize(ConferenceManager conferenceManager, CallHandler callHandler, byte mediaPayload,
byte telephoneEventPayload, RtcpReceiver rtcpReceiver) {
this.conferenceManager = conferenceManager;
this.telephoneEventPayload = telephoneEventPayload;
this.rtcpReceiver = rtcpReceiver;
this.callHandler = callHandler;
Logger.writeFile("Call " + cp + " MemberReceiver initialization started..." + cp.getProtocol());
conferenceWhisperGroup = conferenceManager.getWGManager().getConferenceWhisperGroup();
MediaInfo conferenceMediaInfo = conferenceManager.getMediaInfo();
int outSampleRate = conferenceMediaInfo.getSampleRate();
int outChannels = conferenceMediaInfo.getChannels();
jitterManager = new JitterManager("Call " + cp.toString());
if (cp.voiceDetection()) {
if (Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println("Call " + cp + " starting speech Detector...");
}
speechDetector = new SpeechDetector(this.toString(), conferenceMediaInfo);
}
if (cp.getProtocol() != null && ("WebRtc".equals(cp.getProtocol()) || "Rtmfp".equals(cp.getProtocol()) || "Speaker".equals(cp.getProtocol())))
{
conferenceManager.getConferenceReceiver().addMember(this);
if (cp.getJoinConfirmationTimeout() == 0)
{
joinConfirmationReceived = true;
readyToReceiveData = true;
playJoinTreatment();
}
} else {
try {
myMediaInfo = SdpManager.findMediaInfo(mediaPayload);
} catch (ParseException e) {
Logger.println("Call " + cp + " Invalid mediaPayload "
+ mediaPayload);
callHandler.cancelRequest("Invalid mediaPayload " + mediaPayload);
return;
}
Logger.println("My media info: " + myMediaInfo);
int inSampleRate = myMediaInfo.getSampleRate();
int inChannels = myMediaInfo.getChannels();
//if (cp.getPhoneNumber().indexOf("@") >= 0) {
ConferenceReceiver conferenceReceiver = conferenceManager.getConferenceReceiver();
conferenceManager.getConferenceReceiver().addMember(this);
//}
/*
* For input treatments, the treatment manager does the resampling.
*/
if (cp.getInputTreatment() == null) {
if (inSampleRate != outSampleRate || inChannels != outChannels) {
try {
Logger.println("Call " + cp
+ " resample received data from " + inSampleRate + "/"
+ inChannels + " to " + outSampleRate
+ "/" + outChannels);
inSampleRateConverter = new SampleRateConverter(
this.toString(), inSampleRate, inChannels,
outSampleRate, outChannels);
} catch (IOException e) {
callHandler.cancelRequest(e.getMessage());
return;
}
}
}
packet = new RtpReceiverPacket(cp.toString(), myMediaInfo.getEncoding(), inSampleRate, inChannels);
if (initializationDone) {
/*
* This is a re-initialize
*/
return;
}
//if (telephoneEventPayload == 0 && (cp.dtmfDetection() || cp.getJoinConfirmationTimeout() != 0)) {
Logger.println("Call " + cp + " starting dtmf Detector..." + telephoneEventPayload + " " + cp.dtmfDetection());
dtmfDecoder = new DtmfDecoder(this, myMediaInfo);
//}
if (myMediaInfo.getEncoding() == RtpPacket.SPEEX_ENCODING) {
try {
speexDecoder = new SpeexDecoder(inSampleRate, inChannels);
Logger.println("Call " + cp + " created SpeexDecoder");
} catch (SpeexException e) {
Logger.println("Call " + cp + e.getMessage());
callHandler.cancelRequest(e.getMessage());
return;
}
} else if (myMediaInfo.getEncoding() == RtpPacket.PCM_ENCODING) {
try {
opusDecoder = Opus.decoder_create(opusSampleRate, opusChannels);
if (opusDecoder == 0)
{
Logger.println("Call " + cp + " OPUS decoder creation error ");
callHandler.cancelRequest("OPUS decoder creation error ");
return;
}
} catch (Exception e) {
e.printStackTrace();
}
}
if (cp.getJoinConfirmationTimeout() == 0) {
joinConfirmationReceived = true;
readyToReceiveData = true;
playJoinTreatment();
}
if (cp.getInputTreatment() != null &&
cp.getInputTreatment().length() > 0) {
String absolutePath = cp.getInputTreatment();
try {
if (cp.getRecordDirectory() != null) {
absolutePath = Recorder.getAbsolutePath(cp.getRecordDirectory(),
cp.getInputTreatment());
}
if (Logger.logLevel >= Logger.LOG_INFO) {
Logger.println("Call " + cp
+ " New input treatment: " + absolutePath);
}
synchronized (this) {
new InputTreatment(this, absolutePath,
0, conferenceMediaInfo.getSampleRate(),
conferenceMediaInfo.getChannels());
}
} catch (IOException e) {
e.printStackTrace();
Logger.println("MemberReceiver: Invalid input treatment "
+ absolutePath + ": " + e.getMessage());
callHandler.cancelRequest("Invalid input treatment "
+ absolutePath + ": " + e.getMessage());
return;
}
}
String forwardingCallId = cp.getForwardingCallId();
if (forwardingCallId != null) {
CallHandler forwardingCall = CallHandler.findCall(forwardingCallId);
if (forwardingCall == null) {
Logger.println("Invalid forwardingCallId: " + forwardingCallId);
callHandler.cancelRequest("Invalid forwardingCallId: "
+ forwardingCallId);
return;
}
ConferenceMember m = forwardingCall.getMember();
m.getMemberReceiver().addForwardMember(member.getMemberSender());
/*
* If the source of the data is an input treatment, there
* is no need to have the forwarding call receive data
* from the remote side.
*/
if (cp.getInputTreatment() != null) {
m.setConferenceMuted(true);
}
}
}
initializationDone = true;
Logger.writeFile("Call " + cp + " MemberReceiver initialization done...");
}
public void addForwardMember(MemberSender memberSender) {
synchronized (forwardMemberList) {
if (forwardMemberList.contains(memberSender)) {
Logger.println("Already forwarding data to " + memberSender);
return;
}
forwardMemberList.add(memberSender);
}
}
public void removeForwardMember(MemberSender memberSender) {
synchronized (forwardMemberList) {
forwardMemberList.remove(memberSender);
}
}
public void treatmentDoneNotification(TreatmentManager treatmentManager) {
treatmentDoneNotification(treatmentManager.getId());
}
public void treatmentDoneNotification(String treatment) {
synchronized (conferenceManager) {
if (Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println("Input Treatment done " + treatment);
}
if (callHandler == null) {
Logger.println("Call " + cp + " treatment done but no call handler.");
return;
}
CallEvent callEvent = new CallEvent(CallEvent.TREATMENT_DONE);
callEvent.setTreatmentId(treatment);
callHandler.sendCallEventNotification(callEvent);
}
}
public void restartInputTreatment() {
if (Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println("Call " + cp + " restartInputTreatment "
+ cp.getInputTreatment());
}
if (whisperGroup == null) {
Logger.println("Call " + cp + " restartInputTreatment wg is null!");
return;
}
synchronized (this) {
if (cp.getInputTreatment() != null &&
cp.getInputTreatment().length() > 0) {
try {
MediaInfo conferenceMediaInfo =
conferenceManager.getMediaInfo();
String absolutePath = cp.getInputTreatment();
if (cp.getRecordDirectory() != null) {
absolutePath = Recorder.getAbsolutePath(
cp.getRecordDirectory(), cp.getInputTreatment());
}
if (Logger.logLevel >= Logger.LOG_INFO) {
Logger.println("Call " + cp + " new input treatment "
+ absolutePath);
}
new InputTreatment(this, absolutePath, 0,
conferenceMediaInfo.getSampleRate(),
conferenceMediaInfo.getChannels());
} catch (IOException e) {
Logger.println(cp + " Unable to restart input treatment "
+ cp.getInputTreatment() + ": " + e.getMessage());
callHandler.cancelRequest(
"unable to restart input treatment "
+ cp.getInputTreatment() + ": " + e.getMessage());
}
}
}
}
private InputTreatment iTreatment;
private Object lock = new Object();
class InputTreatment extends Thread {
TreatmentManager treatmentManager;
TreatmentDoneListener treatmentDoneListener;
private String treatment;
private int repeatCount;
private int sampleRate;
private int channels;
public InputTreatment(TreatmentDoneListener treatmentDoneListener,
String treatment, int repeatCount, int sampleRate,
int channels) {
this.treatmentDoneListener = treatmentDoneListener;
this.treatment = treatment;
this.repeatCount = repeatCount;
this.sampleRate = sampleRate;
this.channels = channels;
start();
}
public TreatmentManager getTreatmentManager() {
return treatmentManager;
}
public void done() {
if (treatmentManager == null) {
return;
}
treatmentManager.removeTreatmentDoneListener(treatmentDoneListener);
if (Logger.logLevel >= Logger.LOG_INFO) {
Logger.println("Calling stoptreatment for " + treatmentManager);
}
treatmentManager.stopTreatment();
}
public void run() {
synchronized (lock) {
if (iTreatment != null) {
if (iTreatment.getTreatmentManager() != null) {
if (Logger.logLevel >= Logger.LOG_INFO) {
Logger.println("Stopping previous input treatment "
+ iTreatment.getTreatmentManager().getId());
}
iTreatment.done();
} else {
try {
synchronized (iTreatment) {
iTreatment.wait();
}
if (Logger.logLevel >= Logger.LOG_INFO) {
Logger.println(
"Stopping previous input treatment after waiting "
+ iTreatment.getTreatmentManager().getId());
}
iTreatment.done();
} catch (InterruptedException e) {
}
}
}
iTreatment = this;
}
Logger.println("Trying to create treatment manager for "
+ treatment);
try {
treatmentManager = new TreatmentManager(
treatment, repeatCount, sampleRate, channels);
} catch (IOException e) {
Logger.println("MemberReceiver: Invalid input treatment "
+ treatment + ": " + e.getMessage());
callHandler.cancelRequest("Invalid input treatment "
+ treatment + ": " + e.getMessage());
synchronized (this) {
notifyAll();
}
return;
}
treatmentManager.addTreatmentDoneListener(treatmentDoneListener);
if (whisperGroup != null) {
synchronized (whisperGroup) {
inputTreatment = treatmentManager;
}
} else {
inputTreatment = treatmentManager;
}
if (Logger.logLevel >= Logger.LOG_INFO) {
Logger.println("Created treatment manager for "
+ treatmentManager.getId());
}
synchronized (this) {
notifyAll();
}
}
}
public void startInputTreatment(String treatment) {
if (Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println("Call " + cp + " startInputTreatment");
}
cp.setPhoneNumber(treatment);
cp.setInputTreatment(treatment);
restartInputTreatment();
}
public void stopInputTreatment() {
if (Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println("Call " + cp + " stopInputTreatment");
}
synchronized (whisperGroup) {
if (inputTreatment != null) {
inputTreatment.stopTreatment();
}
}
}
private boolean datagramChannelRegistered;
public SelectionKey register(Selector selector) throws IOException {
try {
selectionKey =
datagramChannel.register(selector, SelectionKey.OP_READ);
} catch (ClosedChannelException e) {
callHandler.cancelRequest("register failed, channel closed!");
throw new IOException("register failed, channel closed!");
} catch (Exception e) {
Logger.println("register exception! " + e.getMessage());
throw new IOException("register exception! " + e.getMessage());
}
datagramChannelRegistered = true;
selectionKey.attach(this);
return selectionKey;
}
public void unregister() {
if (selectionKey != null) {
selectionKey.cancel();
selectionKey = null;
}
}
public int getLinearBufferSize() {
return RtpPacket.HEADER_SIZE + packet.getDataSize();
}
/*
* Reset detectors if no packets are received.
* Cancel call if no RTP or RTCP packets are received.
*/
private int noDataCount;
public boolean checkPacketsReceived() {
if (callCancelled) {
return false;
}
if (callIsDead()) {
return false;
}
/*
* 3 packets should be enough for the speech detector
* and dtmf detector to know someone isn't speaking.
* After we've given the detector 60 ms of silence pakcets,
* we don't need to send it any more packets.
*/
int last = lastMediaPacketsReceived;
lastMediaPacketsReceived = mediaPacketsReceived;
if (last != mediaPacketsReceived) {
noDataCount = 0;
return true;
}
noDataCount++;
if (noDataCount != 3) {
return true;
}
/*
* Reset previous samples in sampleRateConverter
*/
if (inSampleRateConverter != null) {
inSampleRateConverter.reset();
}
/*
* we haven't received any data for 3 packet periods (60 ms).
* Make sure the speech detector knows we're not talking
*/
if (speechDetector != null) {
if (speechDetector.reset()) {
callHandler.speakingChanged(false);
}
}
/*
* Make sure the dtmf detector knows there's silence
*/
if (dtmfDecoder != null) {
String dtmfKeys = dtmfDecoder.noDataReceived();
if (dtmfKeys != null) {
Logger.println("silence. dtmf " + dtmfKeys);
processDtmfKeys(dtmfKeys);
}
}
return true;
}
private boolean callCancelled;
private boolean callIsDead() {
if (cp.getProtocol() != null && ("WebRtc".equals(cp.getProtocol()) || "Rtmfp".equals(cp.getProtocol()) || "Speaker".equals(cp.getProtocol())))
{
return false;
}
if (RtpSocket.getRtpTimeout() == 0) {
return false; // no timeout
}
String phoneNumber = cp.getPhoneNumber();
if (phoneNumber != null && phoneNumber.indexOf("6666@") >= 0) {
return false; // don't timeout calls to the bridge.
}
/*
* Only do this for sip calls. For some reason, other calls
* are getting timed out if this "if" is removed.
*/
if (cp.isDistributedBridge() == true || phoneNumber.indexOf("sip:") < 0 || phoneNumber.indexOf("tel:") < 0) {
return false;
}
long rtpElapsed;
if (timeCurrentPacketReceived == 0) {
rtpElapsed = 0;
} else {
rtpElapsed = (System.currentTimeMillis() - timeCurrentPacketReceived) / 1000;
}
long rtcpElapsed = rtcpReceiver.secondsSinceLastReport(member.getRtcpAddress());
if (rtcpElapsed < RtpSocket.getRtpTimeout() || rtpElapsed < RtpSocket.getRtpTimeout()) {
return false;
}
//if (Logger.logLevel >= Logger.LOG_INFO) {
Logger.println("Call " + cp
+ " time since last RTCP report " + rtcpElapsed
+ " time since last RTP packet received " + rtpElapsed);
//}
/*
* We have not received an RTP or RTCP packet in quite some
* time. Assume the call is dead.
*
* XXX There is a gateway (10.6.4.61)
* which doesn't send RTCP packets.
* For now, we'll only timeout calls with "sip:" in
* the phone number.
*/
Logger.println("Call " + cp
+ ": Timeout, cancelling the call...");
callHandler.cancelRequest("call timeout, no keepalive received");
callCancelled = true;
return true;
}
/**
* Handle data which the ConferenceReceiver has sent to us and
* add it to the current whisper group mix.
*
* There is a single ConferenceReceiver to which all CallParticipants
* send data. The ConferenceReceiver dispatches data to
* the appropriate conference member by calling our receive method.
*
* The job of the conference member is to receive packets from
* the call participant and add them to the appropriate whisper group.
*
* Each MemberReceiver keeps it's own list of data it has contributed
* to the mix's linearMixBuffers.
*
* When a member receives data from the CallParticipant, the member
* adds an element of int[] to its own list. The member then adds
* the data to the current whisper group. The element index
* of the member's list is the same index used for the whisper group's
* linearMixBuffers.
*
* Adding and removing list elements is done synchronized on whisperGroup.
*/
public void setDropPackets(int dropPackets) {
this.dropPackets = dropPackets;
}
/*
* With deferMixing set to true, when a packet arrives it is inserted
* in the jitter buffer and mixing is done in saveCurrentContribution().
* It may be better to do the mixing when the packet is received so
* the work is done by a thread separate from the sender thread.
*/
private static boolean deferMixing = false;
private int forcedToDeferMixing;
public static void deferMixing(boolean deferMixing) {
MemberReceiver.deferMixing = deferMixing;
}
public static boolean deferMixing() {
return deferMixing;
}
private void forwardData(int[] data) {
for (MemberSender memberSender : forwardMemberList) {
if (Logger.logLevel == -88) {
Logger.println("Forwarding " + data.length + " to "
+ memberSender);
}
if (memberSender.memberIsReadyForSenderData()) {
memberSender.sendData(data);
}
}
}
public void receive(InetSocketAddress fromAddress, byte[] receivedData, int length) {
member.getMemberSender().setSendAddress(fromAddress);
if (packet == null) return;
/*
* receivedData has a 12 byte RTP header at the beginning
* and length includes the RTP header.
*/
timeCurrentPacketReceived = System.currentTimeMillis();
packetsReceived++;
if (packetsReceived == 1) {
Logger.println("Call " + cp + " got first packet, length "
+ length);
packet.setBuffer(receivedData);
/*
* TODO: Get the synchonization source for this call.
*/
}
if (cp.getInputTreatment() != null) {
return;
}
if (dropPackets != 0) {
if ((packetsReceived % dropPackets) == 0) {
return;
}
}
/*
* For debugging
*/
if (traceCall || Logger.logLevel == -11) {
Logger.writeFile("Call " + cp + " got packet, len " + length);
}
/*
* Decrypt data if it's encrypted
*/
long start = 0;
if (decryptCipher != null) {
if (traceCall || Logger.logLevel == -1) {
start = System.nanoTime();
}
receivedData = decrypt(receivedData);
if (traceCall || Logger.logLevel == -1) {
Logger.println("Call " + cp + " decrypt time "
+ ((System.nanoTime() - start) / 1000000000.)
+ " seconds");
}
}
recordPacket(receivedData, length);
packet.setBuffer(receivedData);
packet.setLength(length);
byte payload = packet.getRtpPayload();
int elapsedTime = (int)
(timeCurrentPacketReceived - timePreviousPacketReceived);
if (gotComfortPayload || packetsReceived == 1) {
/*
* We don't want to count the time when the remote stopped
* sending to us.
*/
packet.setMark(); // make sure MARK bit is set
if (gotComfortPayload) {
gotComfortPayload = false;
if (traceCall || Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println("Call " + cp
+ " received packet after comfort payload");
}
}
}
if (packet.isMarkSet() == true) {
elapsedTime = RtpPacket.PACKET_PERIOD;
}
totalTime += elapsedTime;
synchronized (jitterManager) {
/*
* Insert place holder for this packet
*/
jitterManager.insertPacket(packet.getRtpSequenceNumber(),
elapsedTime);
}
int rtpTimestampAdjustment = length - RtpPacket.HEADER_SIZE;
if (payload == RtpPacket.COMFORT_PAYLOAD || payload == 19) {
/*
* Asterisk seems to have a bug in which the bridge offers
* 13 decimal as the comfort payload and asterisk replies with
* 13 hex (19 decimal).
* For now, we'll treat 19 as the comfort noise payload as well.
*/
receiveComfortPayload(packet, elapsedTime);
if (inSampleRateConverter != null) {
inSampleRateConverter.reset();
}
if (speechDetector != null) {
if (speechDetector.isSpeaking()) {
callHandler.speakingChanged(false);
}
speechDetector.reset();
}
} else if (payload == 18) {
/*
* We sometimes get payload 18 which is undefined according to
* the RFC. The data looks like audio data.
* But for now, we just drop the packet.
*/
Logger.error("Call " + cp + " unexpected payload " + payload
+ " dropping packet ");
Util.dump("bad payload 18 data", packet.getData(), 0, 16);
} else if (payload == myMediaInfo.getPayload()) {
if (traceCall || Logger.logLevel == -1) {
start = System.nanoTime();
}
try {
rtpTimestampAdjustment = receiveMedia(receivedData, length);
} catch (SpeexException e) {
Logger.println("speex decorder failed: " + e.getMessage());
e.printStackTrace();
callHandler.cancelRequest("Call " + cp + e.getMessage());
return;
}
if (traceCall || Logger.logLevel == -1) {
Logger.println("Call " + cp + " receiveMedia time "
+ ((System.nanoTime() - start) / 1000000000.)
+ " seconds");
}
int processTime = (int)
(System.currentTimeMillis() - timeCurrentPacketReceived);
timeToProcessMediaPackets += processTime;
mediaPacketsReceived++;
} else if (payload != 0 && payload == telephoneEventPayload) {
if (cp.ignoreTelephoneEvents() == false) {
receiveDtmfPayload(packet);
}
} else {
if ((badPayloads % 1000) == 0) {
badPayloads++;
Logger.error("Call " + cp + " unexpected payload " + payload
+ " length " + length);
Util.dump("unexpected payload", receivedData, 0, 16);
}
if (badPayloads >= 1000 && mediaPacketsReceived == 0) {
callHandler.cancelRequest("Call " + cp
+ " bad media payload being sent by call");
}
}
packet.updateRtpHeader(rtpTimestampAdjustment);
timePreviousPacketReceived = timeCurrentPacketReceived;
}
private void receiveComfortPayload(RtpReceiverPacket packet,
int elapsedTime) {
comfortNoiseLevel = packet.getComfortNoiseLevel();
if (traceCall || Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println("Call " + cp
+ ": received comfort payload, level " + comfortNoiseLevel
+ " sequence " + packet.getRtpSequenceNumber());
}
comfortPayloadsReceived++;
if (Logger.logLevel >= Logger.LOG_DEBUG) {
log(packet);
}
}
private int receiveMedia(byte[] receivedData, int length)
throws SpeexException {
long start = 0;
int[] data = decodeToLinear(receivedData, length);
if (inputVolume != 1.0) {
callHandler.getMember().adjustVolume(data, inputVolume);
}
//Logger.println("Call " + cp + " receiveMedia length " + length + " decoded int length " + data.length);
int numberOfSamples = data.length;
if (myMediaInfo.getEncoding() == RtpPacket.PCMU_ENCODING) {
/*
* The cisco gateway often gives us short packets
* right before a comfort payload
*/
numberOfSamples = length - RtpPacket.HEADER_SIZE;
}
if (traceCall || Logger.logLevel == -1) {
start = System.nanoTime();
}
if (inSampleRateConverter != null) {
if (traceCall || Logger.logLevel == -1) {
start = System.nanoTime();
}
/*
* XXX We never downsample here because the bridge
* will never advertise a sample rate higher than
* that of the conference.
*/
try {
data = inSampleRateConverter.resample(data);
//Logger.println("length after resample " + data.length);
} catch (IOException e) {
Logger.println("Call " + cp + " can't resample received data " + e.getMessage());
callHandler.cancelRequest("Call " + cp
+ "can't resample received data " + e.getMessage());
return 0;
}
if (traceCall || Logger.logLevel == -1) {
Logger.println("Call " + cp + " resample time "
+ ((System.nanoTime() - start) / 1000000000.)
+ " seconds");
}
}
if (traceCall || Logger.logLevel == -1) {
start = System.nanoTime();
}
/*
* If there are calls to other bridges which need the data
* from this member, then we send that data right now.
* This reduced latency because this is before we put the
* data in the jitter buffer.
*/
forwardData(data);
/*
* data is a int[] with no RTP header
*/
handleMedia(data, packet.getRtpSequenceNumber());
if (traceCall || Logger.logLevel == -1) {
Logger.println("Call " + cp + " handleMedia time "
+ ((System.nanoTime() - start) / 1000000000.)
+ " seconds");
}
if (Logger.logLevel >= Logger.LOG_DEBUG) {
log(packet);
}
return numberOfSamples;
}
private int[] decodeToLinear(byte[] receivedData, int length) throws SpeexException
{
/*
* receivedData has the 12 byte RTP header.
*/
int[] data = new int[myMediaInfo.getSamplesPerPacket()];
long start = 0;
if (myMediaInfo.getEncoding() == RtpPacket.PCMU_ENCODING)
{
if (traceCall || Logger.logLevel == -1)
{
start = System.nanoTime();
}
/*
* Convert ulaw data to linear. length is the ulaw
* data length plus the RTP header length.
*
* If the incoming packet is shorter, than we expect,
* the rest of <data> will be filled with 0 * which is PCM_SILENCE.
*/
AudioConversion.ulawToLinear(receivedData, RtpPacket.HEADER_SIZE, length - RtpPacket.HEADER_SIZE, data);
if (length < 172 && Logger.logLevel >= Logger.LOG_DETAIL) {
Logger.println("Call " + cp + " received short packet " + length);
}
if (traceCall || Logger.logLevel == -1) {
Logger.println("Call " + cp + " ulawToLinear time " + ((System.nanoTime() - start) / 1000000000.) + " seconds");
}
} else if (myMediaInfo.getEncoding() == RtpPacket.PCM_ENCODING) {
int inputOffset = RtpPacket.HEADER_SIZE;
int inputLength = length - RtpPacket.HEADER_SIZE;
int frameSizeInSamplesPerChannel = Opus.decoder_get_nb_samples(opusDecoder, receivedData, inputOffset, inputLength);
if (frameSizeInSamplesPerChannel > 1)
{
int frameSizeInBytes = outputFrameSize * opusChannels * frameSizeInSamplesPerChannel;
byte[] output = new byte[frameSizeInBytes];
frameSizeInSamplesPerChannel = Opus.decode(opusDecoder, receivedData, inputOffset, inputLength, output, 0, frameSizeInSamplesPerChannel, 0);
data = AudioConversion.bytesToLittleEndianInts(output);
}
} else if (myMediaInfo.getEncoding() == RtpPacket.SPEEX_ENCODING) {
if (traceCall || Logger.logLevel == -1) {
start = System.nanoTime();
}
data = speexDecoder.decodeToIntArray(receivedData, RtpPacket.HEADER_SIZE, length - RtpPacket.HEADER_SIZE);
if (traceCall || Logger.logLevel == -1)
{
Logger.println("Call " + cp + " speex decode time " + ((System.nanoTime() - start) / 1000000000.) + " seconds");
}
} else {
AudioConversion.bytesToInts(receivedData, RtpPacket.HEADER_SIZE,
length - RtpPacket.HEADER_SIZE, data);
}
return data;
}
public synchronized void handleVP8Video(RTPPacket videoPacket)
{
ArrayList<ConferenceMember> memberList = conferenceManager.getMemberList();
for (ConferenceMember member : memberList)
{
if (member == this.member) {
continue;
}
member.getMemberSender().handleVP8Video(videoPacket);
}
}
/*
* data is a int[] with no RTP data and has been decoded
* and resampled to the conference sample rate.
*/
public synchronized void handleWebRtcMedia(int[] data, short sequenceNumber)
{
if (readyToReceiveData() == false) return;
timeCurrentPacketReceived = System.currentTimeMillis();
int elapsedTime = (int) (timeCurrentPacketReceived - timePreviousPacketReceived);
synchronized (jitterManager) {
jitterManager.insertPacket(sequenceNumber, elapsedTime);
}
if (inputVolume != 1.0) {
callHandler.getMember().adjustVolume(data, inputVolume);
}
handleMedia(data, sequenceNumber);
timePreviousPacketReceived = timeCurrentPacketReceived;
}
private void handleMedia(int[] data, short sequenceNumber)
{
if (dtmfDecoder != null) {
if (checkDtmf(data) == true) {
if (traceCall || Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.writeFile("Call " + cp
+ " checkDtmf returned true, data length "
+ data.length);
}
return;
}
}
if (isMuted()) {
if (speechDetector != null &&
cp.voiceDetectionWhileMuted() == true) {
/*
* Speech detection may be useful for PDA's in a
* conference room. Even though you wouldn't want
* the voice from the PDA microphone to be added to the mix,
* it would be useful for members not in the conference room
* to know who is speaking.
*/
if (speechDetector.processData(data) == true) {
callHandler.speakingChanged(speechDetector.isSpeaking());
}
}
return;
}
if (relayChannel != null)
{
try {
relayChannel.pushReceiverAudio(data);
} catch(Exception e) {}
return;
}
long start = 0;
if (traceCall || Logger.logLevel == -1) {
start = System.nanoTime();
}
synchronized (whisperGroup) {
if (traceCall || Logger.logLevel == -1) {
Logger.println("Call " + cp + " handleMedia lock wait time "
+ ((System.nanoTime() - start) / 1000000000.)
+ " seconds");
}
if (joinConfirmationReceived == false) {
/*
* Drop this packet. We're still waiting for confirmation.
*/
return;
}
synchronized (jitterManager) {
jitterManager.insertPacket(sequenceNumber, data);
}
if (deferMixing == false) {
if (contributionValid) {
forcedToDeferMixing++;
} else {
saveCurrentContribution();
}
}
}
if (speechDetector != null) {
if (speechDetector.processData(data) == true) {
callHandler.speakingChanged(speechDetector.isSpeaking());
}
}
}
private boolean checkDtmf(int[] data) {
String dtmfKeys = dtmfDecoder.processData(data);
if (CallHandler.dtmfSuppression() == true &&
cp.dtmfSuppression() == true) {
if (dtmfDecoder.dtmfDetected()) {
if (isAutoMuted == false) {
if (traceCall || Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println("Call " + cp
+ " dtmf detected, setting automute ");
}
isAutoMuted = true;
flushContributions();
}
} else {
if (isAutoMuted == true) {
if (traceCall || Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println("Call " + cp + " automute now false");
}
}
isAutoMuted = false;
}
}
if (dtmfKeys != null) {
processDtmfKeys(dtmfKeys);
if (traceCall || Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println("Call " + cp + " processed dtmf packet"
+ " with key " + dtmfKeys
+ " dtmfPackets " + dtmfPackets);
}
isAutoMuted = false;
return true; // drop this packet
}
if (isAutoMuted) {
return true;
}
return false;
}
/*
* process dtmf payload
*/
private long dtmfTimestamp = 0;
private void receiveDtmfPayload(RtpReceiverPacket packet) {
byte[] data = packet.getData();
if (traceCall || Logger.logLevel >= Logger.LOG_MOREINFO) {
Util.dump("received telephoneEventPayload", data, 0, 16);
}
/*
* First byte of data is the dtmf key
*/
if (packet.isDtmfEndSet()) {
/*
* Very strange packets come from the Cisco gateway.
* The first several have the end bit set followed
* by a number which don't have the bit set.
* Fortunately, all of the packets have the same timestamp
* so we can filter on that.
*/
if (traceCall || Logger.logLevel >= Logger.LOG_MOREINFO) {
Util.dump("Dtmf end set, ts " + Long.toHexString(dtmfTimestamp)
+ " pkt ts " + Long.toHexString(packet.getRtpTimestamp()),
data, 0, 16);
}
if (dtmfTimestamp != packet.getRtpTimestamp()) {
dtmfTimestamp = packet.getRtpTimestamp();
/*
* Key has been released
* Now it's time to process the key
*/
String dtmfKey = String.valueOf((int)data[RtpPacket.DATA]);
if (data[RtpPacket.DATA] == 10) {
dtmfKey = "*";
} else if (data[RtpPacket.DATA] == 11) {
dtmfKey = "#";
}
processDtmfKeys(dtmfKey);
}
} else {
/*
* Key is still pressed
*/
if (traceCall || Logger.logLevel >= Logger.LOG_MOREINFO) {
Util.dump("Got dtmf key payload key still pressed: ", data, 0, 16);
}
}
}
public static void setForwardDtmfKeys(boolean forwardDtmfKeys) {
MemberReceiver.forwardDtmfKeys = forwardDtmfKeys;
}
public static boolean getForwardDtmfKeys() {
return forwardDtmfKeys;
}
private ArrayList joinConfirmationListeners = new ArrayList();
public void addJoinConfirmationListener(JoinConfirmationListener listener) {
synchronized (joinConfirmationListeners) {
joinConfirmationListeners.add(listener);
}
}
public void removeJoinConfirmationListener(
JoinConfirmationListener listener) {
synchronized (joinConfirmationListeners) {
joinConfirmationListeners.remove(listener);
}
}
private void notifyJoinConfirmationListeners() {
synchronized (joinConfirmationListeners) {
for (int i = 0; i < joinConfirmationListeners.size(); i++) {
JoinConfirmationListener listener = (JoinConfirmationListener)
joinConfirmationListeners.get(i);
listener.joinConfirmation();
removeJoinConfirmationListener(listener);
}
}
}
/*
* This is called when the dtmfDecoder detects a dtmf key.
*/
private void processDtmfKeys(String dtmfKeys) {
dtmfPackets++;
Logger.println("Call " + cp + " got dtmf key " + dtmfKeys);
if (joinConfirmationReceived == false) {
if (!dtmfKeys.equals(joinConfirmationKey)) {
Logger.println("Call " + cp
+ " jc false, dtmfKeys " + dtmfKeys
+ " != " + joinConfirmationKey);
return;
}
joinConfirmationReceived = true;
readyToReceiveData = true;
notifyJoinConfirmationListeners();
Logger.println("Call " + cp + " join confirmation received");
playJoinTreatment();
} else {
/*
* If the member is whispering in a whisper group
* forward the dtmf key to the other members.
*
* This is intended for outgoing calls so that
* someone can call AT&T conferencing for example,
* and enter the meeting code.
*/
if (forwardDtmfKeys == true) {
if (whisperGroup != conferenceWhisperGroup) {
Logger.writeFile("Call " + cp
+ " Forwarding dtmf key " + dtmfKeys);
whisperGroup.forwardDtmf(this, dtmfKeys);
}
}
}
if (cp.dtmfDetection() == false) {
/*
* We enabled dtmf detection only so that the member could
* confirm that it wants to join the conference. Once confirmed,
* dtmf detection is disabled only detection was explicitly enabled.
*/
dtmfDecoder = null;
}
callHandler.dtmfKeys(dtmfKeys);
}
/**
* Play audio treatment to all conference members indicating that
* a member has joined the conference.
*/
private void playJoinTreatment() {
String joinTreatment;
if ((joinTreatment = cp.getConferenceJoinTreatment()) != null) {
Logger.writeFile("Call " + cp
+ ": playing conference join treatment " + joinTreatment);
try {
conferenceManager.addTreatment(joinTreatment);
} catch (IOException e) {
Logger.println("Call " + cp
+ " failed to start join treatment " + joinTreatment);
}
}
}
/*
* Flush contributions to suppress dtmf sounds.
* This doesn't work very well unless the buffers for the dtmf
* sounds are ahead of the sender. It takes 40ms of data to detect
* a dtmf key so it's quite possible the first 20ms have already been
* sent out.
*/
public void flushContributions() {
if (whisperGroup == null) {
return;
}
}
public void setInputVolume(double inputVolume) {
this.inputVolume = inputVolume;
}
public double getInputVolume() {
return inputVolume;
}
private TreatmentManager inputTreatment;
private int[] previousContribution;
private int[] currentContribution;
private boolean contributionValid = false;
public String getSourceId() {
return cp.getCallId();
}
public boolean contributionIsInCommonMix() {
return whisperGroup != null && whisperGroup.hasCommonMix();
}
public int[] getPreviousContribution() {
return previousContribution;
}
public int[] getCurrentContribution() {
return currentContribution;
}
public void invalidateCurrentContribution() {
synchronized (whisperGroup) {
previousContribution = currentContribution;
currentContribution = null;
contributionValid = false;
}
}
public void saveCurrentContribution() {
if (readyToReceiveData == false || whisperGroup == null) {
previousContribution = null;
currentContribution = null;
return;
}
synchronized (whisperGroup) {
if (contributionValid) {
return;
}
currentContribution = null;
contributionValid = true;
if (inputTreatment == null) {
synchronized (jitterManager) {
try {
JitterObject jo = jitterManager.getFirstPacket();
currentContribution = (int[]) jo.data;
} catch (NoSuchElementException e) {
}
}
} else {
/*
* If there's an input treatment, there's no endpoint
* and therefore no member contribution other than
* the input treatment
*/
inputTreatment.saveCurrentContribution();
currentContribution = inputTreatment.getCurrentContribution();
if (currentContribution == null) {
if (Logger.logLevel >= Logger.LOG_INFO) {
Logger.println("Call " + cp
+ " input treatment returned null");
}
inputTreatment = null;
} else {
forwardData(currentContribution);
if (inputVolume != 1) {
callHandler.getMember().adjustVolume(currentContribution, inputVolume);
}
}
if (speechDetector != null) {
if (currentContribution == null) {
if (speechDetector.reset() == true) {
callHandler.speakingChanged(false);
}
} else {
if (speechDetector.processData(currentContribution) ==
true) {
boolean isSpeaking = speechDetector.isSpeaking();
callHandler.speakingChanged(isSpeaking);
if (isSpeaking == false) {
currentContribution = null;
}
}
}
}
}
if (currentContribution != null) {
/*
* Add this packet's data to the appropriate whisperGroup
*/
if (whisperGroup.hasCommonMix()) {
whisperGroup.addToLinearDataMix(currentContribution,
doNotRecord());
}
recordAudio(currentContribution, currentContribution.length);
if (Logger.logLevel == -89) {
Logger.println("Call " + cp
+ " MemberReceiver contributed");
}
}
}
}
private void log(RtpReceiverPacket rtpPacket) {
long now = System.currentTimeMillis();
long rtpTimestampChange = rtpPacket.getRtpTimestamp() -
previousRtpTimestamp;
previousRtpTimestamp = rtpPacket.getRtpTimestamp();
String summary = "";
String flags = "";
String badTime = " ";
String badTimestamp = " ";
if (rtpPacket.isMarkSet()) {
flags = "MARK ";
} else {
if (packetsReceived > 1) {
if (now - timePreviousPacketReceived < 15) {
badTime = "-";
summary = "!";
} else if (now - timePreviousPacketReceived > 25) {
badTime = "+";
summary = "!";
}
if (rtpTimestampChange > myMediaInfo.getSamplesPerPacket()) {
badTimestamp = ">";
summary = "!";
} else if (rtpTimestampChange < myMediaInfo.getSamplesPerPacket()) {
badTimestamp = "<";
summary = "!";
}
}
}
if (rtpPacket.getRtpPayload() == RtpPacket.COMFORT_PAYLOAD) {
flags += "COMFORT ";
}
String timestamp = Integer.toHexString(
(int)(rtpPacket.getRtpTimestamp() & 0xffffffff));
if (timestamp.length() != 8) {
timestamp += " "; // for alignment
}
Logger.writeFile("R "
+ (now - timePreviousPacketReceived) + badTime
+ "\t" + Integer.toHexString(
(int)(rtpTimestampChange & 0xffffffff))
+ badTimestamp
+ "\t" + Integer.toHexString(rtpPacket.getRtpSequenceNumber())
+ "\t" + timestamp
+ "\t" + flags + cp + " R" + summary);
}
/**
* Member is leaving a conference. Print statistics for the member.
*/
public void end() {
if (done) {
return;
}
done = true;
if (speechDetector != null && speechDetector.isSpeaking()) {
callHandler.speakingChanged(false);
}
synchronized (recordingLock) {
if (recorder != null) {
recorder.done();
recorder = null;
}
}
readyToReceiveData = false;
if (datagramChannelRegistered && datagramChannel != null) {
try {
datagramChannel.close();
if (Logger.logLevel >= Logger.LOG_DETAIL) {
Logger.println("Call " + cp + " closed datagramChannel "
+ datagramChannel);
}
datagramChannel = null;
} catch (IOException e) {
Logger.println("Call " + cp
+ " exception closing datagram channel " + e.getMessage());
}
} else {
Logger.println("Call " + cp + " not closing datagramChannel");
}
if (joinConfirmationReceived == true) {
String leaveTreatment;
/**
* Play audio treatment to all conference members indicating that
* a member has left the conference.
*/
if ((leaveTreatment = cp.getConferenceLeaveTreatment()) != null) {
try {
conferenceManager.addTreatment(leaveTreatment);
} catch (IOException e) {
Logger.println("Call " + cp
+ " failed to start leave treatment " + leaveTreatment);
}
}
}
if (opusDecoder != 0)
{
Opus.decoder_destroy(opusDecoder);
opusDecoder = 0;
}
}
public void printStatistics() {
if (conferenceManager == null) {
return;
}
synchronized (conferenceManager) {
if (packet == null) {
return;
}
Logger.writeFile("Call " + cp + ": "
+ packetsReceived + " packets received");
Logger.writeFile("Call " + cp + ": "
+ packet.getShortPackets() + " short packets");
Logger.writeFile("Call " + cp + ": "
+ packetsDropped + " packets dropped");
Logger.writeFile("Call " + cp + ": "
+ packet.getOutOfSequencePackets()
+ " out of sequence packets");
Logger.writeFile("Call " + cp + ": "
+ packet.getWrongRtpTimestamp() + " incorrect RTP timestamp");
Logger.writeFile("Call " + cp + ": " + comfortPayloadsReceived
+ " comfort payloads received");
Logger.writeFile("Call " + cp + ": Forced to defer mixing "
+ forcedToDeferMixing);
if (packetsReceived != 0) {
Logger.writeFile("Call " + cp + ": "
+ "total time " + totalTime);
Logger.writeFile("Call " + cp + ": "
+ ((float)totalTime / (double)packetsReceived)
+ " average milliseconds between receiving packets");
Logger.writeFile("Call " + cp + ": "
+ ((float)timeToProcessMediaPackets /
(float)mediaPacketsReceived)
+ " average milliseconds to process a media packet");
}
Logger.writeFile("Call " + cp + ": "
+ decryptCount + " packets decrypted");
if (decryptCount != 0) {
Logger.writeFile("Call " + cp + ": "
+ (((float)decryptTime / (float)decryptCount) / 1000)
+ " microseconds average per decrypt");
}
if (speexDecoder != null) {
int decodes = speexDecoder.getDecodes();
long decodeTime = speexDecoder.getDecodeTime();
if (decodes > 0) {
Logger.writeFile("Call " + cp + ": "
+ "Average Speex decode time "
+ (((float)decodeTime / decodes) / 1000000000.)
+ " seconds");
}
}
if (inSampleRateConverter != null) {
inSampleRateConverter.printStatistics();
}
if (jitterManager != null) {
synchronized (jitterManager) {
jitterManager.printStatistics();
}
}
Logger.writeFile("");
if (dtmfDecoder != null) {
dtmfDecoder.printStatistics();
Logger.writeFile("");
}
if (speechDetector != null) {
speechDetector.printStatistics();
Logger.writeFile("");
}
Logger.flush();
}
}
public boolean joinConfirmationReceived() {
return joinConfirmationReceived;
}
/**
* Indicate whether or not this member is ready to receive data
* from the ConferenceReceiver thread.
*/
public boolean readyToReceiveData() {
if (initializationDone == false) {
return false;
}
/*
* We have to allow data in so we can detect a dtmf key for
* join confirmation.
*/
if (joinConfirmationReceived == false) {
return true;
}
if (traceCall) {
if (callHandler.isCallEstablished() == false
|| readyToReceiveData == false) {
Logger.writeFile("readyToReceiveData " + readyToReceiveData +
" established " + callHandler.isCallEstablished());
}
}
return callHandler.isCallEstablished() && readyToReceiveData;
}
private boolean isMuted() {
if (whisperGroup == null) {
return true;
}
if (whisperGroup != conferenceWhisperGroup) {
if (traceCall) {
if (cp.isMuteWhisperGroup()) {
Logger.writeFile("Call " + cp + " whispergroup muted");
}
}
return cp.isMuteWhisperGroup();
}
if (traceCall) {
if (isAutoMuted || cp.isMuted() || cp.isConferenceMuted() ||
cp.isConferenceSilenced()) {
Logger.writeFile("Call " + cp + " isAutoMuted " + isAutoMuted
+ " isMuted " + cp.isMuted()
+ " isConf muted " + cp.isConferenceMuted()
+ " isConf sileneced " + cp.isConferenceSilenced());
}
}
return isAutoMuted || cp.isMuted() || cp.isConferenceMuted() ||
cp.isConferenceSilenced();
}
/**
* Mute or unmute a member
*
* @param isMuted boolean true if member should be muted, false otherwise.
*/
public void setMuted(boolean isMuted) {
if (traceCall || Logger.logLevel >= Logger.LOG_INFO) {
Logger.println("Call " + cp + " mute is now " + isMuted);
}
cp.setMuted(isMuted);
if (speechDetector == null) {
return;
}
if (isMuted) {
if (speechDetector.isSpeaking()) {
callHandler.speakingChanged(false);
}
}
speechDetector.reset();
}
/**
* Mute or unmute a member from a whisper group
*
* @param isMuted boolean true if member should be muted, false otherwise.
*/
public void setMuteWhisperGroup(boolean isMuteWhisperGroup) {
if (traceCall || Logger.logLevel >= Logger.LOG_INFO) {
Logger.println("Call " + cp + " muteWhisperGroup is now "
+ isMuteWhisperGroup);
}
cp.setMuteWhisperGroup(isMuteWhisperGroup);
if (isMuteWhisperGroup) {
synchronized (whisperGroup) {
flushContributions();
}
}
if (speechDetector == null) {
return;
}
if (isMuteWhisperGroup) {
if (speechDetector.isSpeaking()) {
callHandler.speakingChanged(false);
}
}
speechDetector.reset();
}
public void setPowerThresholdLimit(double powerThresholdLimit) {
if (speechDetector == null) {
Logger.println("Can't set powerThresholdLimit because there is no "
+ "speech detector");
return;
}
speechDetector.setPowerThresholdLimit(powerThresholdLimit);
}
public void setMinJitterBufferSize(int minJitterBufferSize) {
if (jitterManager == null) {
return;
}
jitterManager.setMinJitterBufferSize(minJitterBufferSize);
}
public void setMaxJitterBufferSize(int maxJitterBufferSize) {
if (jitterManager == null) {
return;
}
jitterManager.setMaxJitterBufferSize(maxJitterBufferSize);
}
public void setPlcClassName(String plcClassName) {
jitterManager.setPlcClassName(plcClassName);
}
public String getPlcClassName() {
return jitterManager.getPlcClassName();
}
public InetSocketAddress getReceiveAddress() {
return new InetSocketAddress(Bridge.getPrivateHost(),
datagramChannel.socket().getLocalPort());
}
private Recorder recorder;
private Integer recordingLock = new Integer(0);
private boolean recordRtp;
public String getFromRecordingFile() {
if (recorder != null) {
return recorder.getRecordPath();
}
return null;
}
private void recordPacket(byte[] data, int length) {
if (cp.getFromRecordingFile() == null) {
return;
}
if (recordRtp == false) {
return;
}
synchronized (recordingLock) {
if (recorder == null) {
return;
}
try {
recorder.writePacket(data, 0, length);
} catch (IOException e) {
Logger.println("Unable to record data " + e.getMessage());
cp.setFromRecordingFile(null);
recorder = null;
}
}
}
private void recordAudio(int[] data, int length) {
if (cp.getFromRecordingFile() == null) {
return;
}
if (recordRtp == true) {
return;
}
synchronized (recordingLock) {
if (recorder == null) {
return;
}
try {
recorder.write(data, 0, length);
} catch (IOException e) {
Logger.println("Unable to record data " + e.getMessage());
cp.setFromRecordingFile(null);
recorder = null;
}
}
}
public boolean doNotRecord() {
return cp.doNotRecord();
}
public void setDoNotRecord(boolean doNotRecord) {
cp.setDoNotRecord(doNotRecord);
Logger.println("Call " + cp + " doNotRecord is " + doNotRecord);
}
public void setRecordFromMember(boolean enabled, String recordingFile,
String recordingType) throws IOException {
if (doNotRecord()) {
if (enabled) {
Logger.println("Call " + cp + " doesn't allow recording.");
enabled = false;
}
}
if (recorder != null) {
recorder.done();
recorder = null;
}
synchronized (recordingLock) {
if (enabled == false) {
cp.setFromRecordingFile(null);
return;
}
if (recordingType == null) {
recordingType = "Au";
}
recordRtp = false;
if (recordingType.equalsIgnoreCase("Rtp")) {
recordRtp = true;
}
synchronized (recordingLock) {
MediaInfo m;
try {
if (recordingType.equalsIgnoreCase("Rtp")) {
m = SdpManager.findMediaInfo(
myMediaInfo.getEncoding(),
myMediaInfo.getSampleRate(),
myMediaInfo.getChannels());
} else {
m = SdpManager.findMediaInfo(
RtpPacket.PCM_ENCODING,
conferenceManager.getMediaInfo().getSampleRate(),
conferenceManager.getMediaInfo().getChannels());
}
} catch (ParseException e) {
Logger.println("Can't record rtp to " + recordingFile
+ " " + e.getMessage());
throw new IOException(e.getMessage());
}
Logger.println("Recording media " + m);
recorder = new Recorder(cp.getRecordDirectory(),
recordingFile, recordingType, m);
cp.setFromRecordingFile(recordingFile);
cp.setFromRecordingType(recordingType);
}
}
}
public WhisperGroup getWhisperGroup() {
return whisperGroup;
}
public void setWhisperGroup(WhisperGroup whisperGroup) {
if (this.whisperGroup != null) {
synchronized (this.whisperGroup) {
flushContributions();
}
}
this.whisperGroup = whisperGroup;
}
public static void setJoinConfirmationKey(String key) {
joinConfirmationKey = key;
}
public static String getJoinConfirmationKey() {
return joinConfirmationKey;
}
private long decryptCount;
private long decryptTime;
private byte[] decrypt(byte[] data) {
try {
decryptCount++;
long start = System.currentTimeMillis();
byte[]clearText = decryptCipher.doFinal(data, 0, data.length);
decryptTime += (System.currentTimeMillis() - start);
return clearText;
} catch (Exception e) {
Logger.println("Call " + cp + " Decryption failed, length "
+ data.length + ": " + e.getMessage());
callHandler.cancelRequest("Decryption failed: "
+ e.getMessage());
return data;
}
}
public String toString() {
return myMemberNumber + " ===> " + cp.toString();
}
public String toAbbreviatedString() {
String callId = myMemberNumber + " ===> " + cp.getCallId();
if (callId.length() < 14) {
return callId;
}
return callId.substring(0, 13);
}
}