/* * 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.CallParticipant; import com.sun.voip.CallEvent; import com.sun.voip.CallEventListener; import com.sun.voip.CallState; import com.sun.voip.Logger; import com.sun.voip.TreatmentDoneListener; import com.sun.voip.TreatmentManager; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketException; import java.util.NoSuchElementException; import java.util.Vector; import java.text.ParseException; /** * Common code for handling incoming outgoing calls. */ public abstract class CallHandler extends Thread { protected ConferenceManager conferenceManager; protected ConferenceMember member; protected MemberSender memberSender; protected MemberReceiver memberReceiver; protected CallSetupAgent csa; protected CallParticipant cp; protected boolean done = false; protected String reasonCallEnded; protected boolean suppressStatus; /* * maintain a list of active calls so individual calls * can be terminated. */ protected static Vector activeCalls = new Vector(); private Vector callEventListeners = new Vector(); /* * One receiver per conference is ideal for scaling. * In order for this to work, we must be able to distinguish * calls based on the source address in packets we receive * from each call. The SIP port exchange only involves destination * ports. For most implmentations (Cisco Gateway, Cisco IP Phone, * conference bridge software) the source and destination ports * are the same. Sip-Communicator is one exception. * * So when we see a sip URI for a target phone number, we create * a separate conference receiver specifically for that call. */ private static boolean oneReceiverPerConference = true; private static int duplicateCallLimit = 100; private static boolean enablePSTNCalls = true; /* * Used by the OutgoingCallHandler to handle two party calls. * This is here so that dtmf keys can be forwarded with two party calls. */ protected CallHandler otherCall; public CallHandler getOtherCall() { return this.otherCall; } public void suppressStatus(boolean suppressStatus) { this.suppressStatus = suppressStatus; } /* * Mostly for debugging */ public String getCallState() { String s = "\n" + cp.toString(); if (member != null) { s += " ConferenceId: " + member.getConferenceManager().getId() + "\n"; s += "\tStarted " + member.getTimeStarted() + "\n"; } else { s += "\n"; } if (csa != null) { s += "\tState = " + csa.getCallState() + "\n"; } else { s += "\tNo Call Setup Agent" + "\n"; } s += "\tIsDistributedBridge " + cp.isDistributedBridge() + "\n"; if (cp.getCallTimeout() == 0) { s += "\tNo timeout\n"; } else { s += "\tCall timeout in " + (cp.getCallTimeout() / 1000) + " seconds\n"; } if (member != null) { s += " " + member.getMemberState(); } return s; } public static String getCallStateForAllCalls() { String s = ""; synchronized(activeCalls) { for (int i = 0; i < activeCalls.size(); i++) { CallHandler call = (CallHandler)activeCalls.elementAt(i); s += call.getCallState() + "\n"; } } return s; } public static String getAllMixDescriptors() { String s = ""; synchronized(activeCalls) { for (int i = 0; i < activeCalls.size(); i++) { CallHandler call = (CallHandler)activeCalls.elementAt(i); s += "MixDescriptors for " + call + "\n"; s += call.getMember().getMixDescriptors() + "\n"; } } return s; } public static String getAllAbbreviatedMixDescriptors() { String s = ""; synchronized(activeCalls) { for (int i = 0; i < activeCalls.size(); i++) { CallHandler call = (CallHandler)activeCalls.elementAt(i); s += "MixDescriptors for " + call + "\n"; s += call.getMember().getAbbreviatedMixDescriptors() + "\n"; } } return s; } /* * Overridden by OutgoingCallhandler. There is no request handler * for incoming calls. */ public abstract CallEventListener getRequestHandler(); public CallParticipant getCallParticipant() { return cp; } /* * Used to switch a call from one conference to another. */ public void setConferenceManager(ConferenceManager conferenceManager) { this.conferenceManager = conferenceManager; } public ConferenceManager getConferenceManager() { return conferenceManager; } public ConferenceMember getMember() { return member; } public MemberSender getMemberSender() { return memberSender; } public MemberReceiver getMemberReceiver() { return memberReceiver; } /* * This method is called by a CallSetupAgent once the endpoint * address is known. The endpoint address is the address from which * we expect to receive RTP packets and to which we will send RTP packets. */ public void setEndpointAddress(InetSocketAddress isa, byte mediaPayload, byte receivePayload, byte telephoneEventPayload) { setEndpointAddress(isa, mediaPayload, receivePayload, telephoneEventPayload, null); } public void setEndpointAddress(InetSocketAddress isa, byte mediaPayload, byte receivePayload, byte telephoneEventPayload, InetSocketAddress rtcpAddress) { member.initialize(this, isa, mediaPayload, receivePayload, telephoneEventPayload, rtcpAddress); } /* * true if call is established */ public boolean isCallEstablished() { if (done || csa == null) { return false; } return csa.isCallEstablished(); } /* * true is call is ending */ public boolean isCallEnding() { if (done || csa == null) { return true; } return csa.isCallEnding(); } public void addCallEventListener(CallEventListener listener) { synchronized (callEventListeners) { callEventListeners.add(listener); } } public void removeCallEventListener(CallEventListener listener) { synchronized (callEventListeners) { callEventListeners.remove(listener); } } public void sendCallEventNotification(CallEvent callEvent) { if (cp.getCallId() != null) { callEvent.setCallId(cp.getCallId()); } else { callEvent.setCallId("CallIdNotInitialized"); } callEvent.setConferenceId(cp.getConferenceId()); callEvent.setCallInfo(cp.getCallOwner()); if (csa != null) { callEvent.setCallState(csa.getCallState()); } else { callEvent.setCallState(new CallState(CallState.UNINITIALIZED)); } synchronized (callEventListeners) { for (int i = 0; i < callEventListeners.size(); i++) { CallEventListener listener = (CallEventListener) callEventListeners.elementAt(i); listener.callEventNotification(callEvent); } } } /* * The subclasses must override this. */ public abstract void callEventNotification(CallEvent callEvent); /** * Send indication when speaker starts or stops speaking. */ public static int totalSpeaking; public void speakingChanged(boolean isSpeaking) { if (isSpeaking) { totalSpeaking++; CallEvent callEvent = new CallEvent(CallEvent.STARTED_SPEAKING); callEvent.setStartedSpeaking(); sendCallEventNotification(callEvent); } else { totalSpeaking--; CallEvent callEvent = new CallEvent(CallEvent.STOPPED_SPEAKING); callEvent.setStoppedSpeaking(); sendCallEventNotification(callEvent); } } public static int getTotalSpeaking() { return totalSpeaking; } /** * Send indication when a dtmf key is pressed */ public void dtmfKeys(String dtmfKeys) { //if (Logger.logLevel >= Logger.LOG_MOREINFO) { Logger.println(cp + " got dtmf keys " + dtmfKeys + " " + cp.dtmfDetection()); //} if (isCallEstablished()) { if (cp.dtmfDetection()) { member.stopTreatment(null); CallEvent callEvent = new CallEvent(CallEvent.DTMF_KEY); callEvent.setDtmfKey(dtmfKeys); sendCallEventNotification(callEvent); } if (otherCall != null) { Logger.println("Call " + cp + " forwarding dtmf key " + dtmfKeys + " to " + otherCall); otherCall.getMemberSender().setDtmfKeyToSend(dtmfKeys); } else { getMemberSender().setDtmfKeyToSend(dtmfKeys); } } else { if (Logger.logLevel >= Logger.LOG_MOREINFO) { Logger.println(cp + " Call not established, ignoring dtmf"); } stopCallAnsweredTreatment(); } } public void stopCallAnsweredTreatment() { if (done || csa == null) { return; } csa.stopCallAnsweredTreatment(); } public void stopCallEstablishedTreatment() { if (done || csa == null) { return; } csa.stopCallEstablishedTreatment(); } /* * terminate a call. */ public void cancelRequest(String reason) { if (done) { return; } done = true; Logger.println(cp + " Cancel request " + reason); if (csa != null) { csa.cancelRequest(reason); } } /* * Add a treatment for the caller */ public void addTreatment(TreatmentManager treatmentManager) { member.addTreatment(treatmentManager); } /* * unique call identifier incremented for each new call. */ private static int callNumber = 0; public static synchronized String getNewCallId() { String s; do { callNumber++; s = String.valueOf(callNumber); String location = Bridge.getBridgeLocation(); if (location.equalsIgnoreCase("Unknown") == false) { s += "_" + Bridge.getBridgeLocation(); } } while (CallHandler.findCall(s) != null); return s; } /** * Find the new call of a call migration. */ public static CallHandler findMigratingCall(String callId) { synchronized(activeCalls) { for (int i = 0; i < activeCalls.size(); i++) { CallHandler call = (CallHandler)activeCalls.elementAt(i); CallParticipant cp = call.getCallParticipant(); if (match(cp, callId) && cp.migrateCall()) { if (Logger.logLevel >= Logger.LOG_DETAIL) { Logger.println("findMigratingCall: found " + callId); } return call; } } } return null; } /** * Find a call by callId. * * Calls are kept in the activeCalls list and uniquely identified * by <callId>::<name>@<phoneNumber> for a phone call and * * This method searches for a call with the callId. */ public static CallHandler findCall(String callId) { if (Logger.logLevel >= Logger.LOG_DETAIL) { Logger.println("findCall: looking for " + callId + ", " + activeCalls.size() + " active calls"); } synchronized(activeCalls) { for (int i = 0; i < activeCalls.size(); i++) { CallHandler call = (CallHandler)activeCalls.elementAt(i); CallParticipant cp = call.getCallParticipant(); if (Logger.logLevel >= Logger.LOG_DETAIL) { Logger.println("findCall: looking for " + callId + " got " + cp.getCallId()); } if (match(cp, callId)) { if (Logger.logLevel >= Logger.LOG_DETAIL) { Logger.println("findCall: found " + callId); } return call; } } } return null; } private static boolean match(CallParticipant cp, String callId) { if (cp.getCallId().equals(callId)) { return true; } if (ConferenceManager.allowShortNames() == false) { return false; } String name = cp.getName(); if (name != null) { if (name.equals(callId)) { return true; } name = name.replaceAll(" ", "_"); if (name.equals(callId)) { return true; } } String number = cp.getPhoneNumber(); if (number == null) { return false; } if (number.equals(callId)) { return true; } if (number.indexOf("sip:") == 0) { int ix = number.indexOf("@"); if (ix >= 0) { number = number.substring(4, ix); if (number.equals(callId)) { return true; } } } return false; } /* * Add call to list of active calls */ public void addCall(CallHandler callHandler) { synchronized(activeCalls) { activeCalls.add(callHandler); // add to list of active calls } } /* * Remove call from list of active calls */ public void removeCall(CallHandler callHandler) { synchronized(activeCalls) { activeCalls.remove(callHandler); // remove call from list Logger.println(""); Logger.println("calls still in progress: " + activeCalls.size()); Logger.println(""); } } /* * End all calls. */ public static void shutdown() { shutdown(0); } public static void shutdown(int delaySeconds) { if (delaySeconds == 0) { /* * Quick shutdown right now! */ hangup("0", "System shutdown"); return; } /* * Notify the active calls that the MC bridge is shutting down */ long start = System.currentTimeMillis(); Logger.println("Shutting down in " + delaySeconds + " seconds"); synchronized(activeCalls) { for (int i = 0; i < activeCalls.size(); i++) { CallHandler call = (CallHandler)activeCalls.elementAt(i); String id = call.getCallParticipant().getCallId(); try { playTreatmentToCall(id, "joinBELL.au;shutdown.au;tts:" + delaySeconds + ";seconds.au"); } catch (IOException e) { Logger.println("Can't play shutdown treatment to call " + id + " " + e.getMessage()); } } /* * Wait at most a minute in case something is severly broken. */ while (System.currentTimeMillis() - start < 60000) { boolean hasTreatments = false; for (int i = 0; i < activeCalls.size(); i++) { CallHandler call = (CallHandler)activeCalls.elementAt(i); hasTreatments = call.getMember().hasTreatments(); if (hasTreatments) { break; } } if (hasTreatments == false) { break; // no treatments left to play } } } if (delaySeconds != 0) { int sleepTime = (int)((delaySeconds * 1000) - (System.currentTimeMillis() - start)); if (sleepTime > 0) { try { Thread.sleep(sleepTime); } catch (InterruptedException e) { } } } hangup("0", "System shutdown"); } /* * Cancel a specified call. If callid is 0, all calls are cancelled. */ public static void hangup(String callId, String reason) { Vector callsToCancel = new Vector(); synchronized(activeCalls) { /* * Make a list of all the calls we want to cancel, then cancel them. * We have to cancel them while not synchronized or * we could deadlock. */ for (int i = 0; i < activeCalls.size(); i++) { CallHandler call = (CallHandler)activeCalls.elementAt(i); CallParticipant cp = call.getCallParticipant(); if (callId.equals("0") || match(cp, callId)) { callsToCancel.add(call); } } } cancel(callsToCancel, reason, false); } public static void hangupOwner(String ownerId, String reason) { Vector callsToCancel = new Vector(); synchronized(activeCalls) { /* * Make a list of all the calls we want to cancel, then cancel them. * We have to cancel them while not synchronized or * we could deadlock. */ for (int i = 0; i < activeCalls.size(); i++) { CallHandler call = (CallHandler)activeCalls.elementAt(i); CallParticipant cp = call.getCallParticipant(); if (cp.getCallOwner().equals(ownerId)) { callsToCancel.add(call); } } } cancel(callsToCancel, reason, false); } public static void suspendBridge() { cancel(activeCalls, "bridge suspended", true); } private static void cancel(Vector callsToCancel, String reason, boolean suppressStatus) { while (callsToCancel.size() > 0) { CallHandler call = (CallHandler)callsToCancel.remove(0); call.suppressStatus(suppressStatus); call.cancelRequest(reason); } } public String getReasonCallEnded() { return reasonCallEnded; } /* * Set cnThresh for the speech detector for a conference member. */ public static void setCnThresh(String callId, int cnThresh) { synchronized(activeCalls) { for (int i = 0; i < activeCalls.size(); i++) { CallHandler call = (CallHandler)activeCalls.elementAt(i); CallParticipant cp = call.getCallParticipant(); if (match(cp, callId)) { MemberReceiver memberReceiver = call.getMemberReceiver(); if (memberReceiver != null) { memberReceiver.setCnThresh(cnThresh); } } } } } /* * force packets to be dropped for debugging. */ public static void setDropPackets(String callId, int dropPackets) { if (callId == null) { return; } synchronized(activeCalls) { for (int i = 0; i < activeCalls.size(); i++) { CallHandler call = (CallHandler)activeCalls.elementAt(i); CallParticipant cp = call.getCallParticipant(); if (match(cp, callId)) { MemberReceiver memberReceiver = call.getMemberReceiver(); if (memberReceiver != null) { memberReceiver.setDropPackets(dropPackets); } } } } } /** * Mute or unmute a conference member. */ public void setMuted(boolean isMuted) { MemberReceiver memberReceiver = getMemberReceiver(); if (memberReceiver != null) { memberReceiver.setMuted(isMuted); } } public static void setMuted(String callId, boolean isMuted) { if (callId == null) { return; } synchronized(activeCalls) { for (int i = 0; i < activeCalls.size(); i++) { CallHandler call = (CallHandler)activeCalls.elementAt(i); CallParticipant cp = call.getCallParticipant(); if (match(cp, callId)) { if (Logger.logLevel >= Logger.LOG_DETAIL) { String s = ""; if (isMuted == false) { s = "un"; } Logger.println(cp.getCallId() + ": " + s + "muted"); } MemberReceiver memberReceiver = call.getMemberReceiver(); if (memberReceiver != null) { memberReceiver.setMuted(isMuted); } } } } } public void setRemoteMediaInfo(String sdp) throws ParseException { csa.setRemoteMediaInfo(sdp); } public static void setRemoteMediaInfo(String callId, String sdp) throws ParseException { synchronized(activeCalls) { for (int i = 0; i < activeCalls.size(); i++) { CallHandler call = (CallHandler)activeCalls.elementAt(i); CallParticipant cp = call.getCallParticipant(); if (match(cp, callId)) { call.setRemoteMediaInfo(sdp); return; } } } throw new ParseException("Invalid callId: " + callId, 0); } /* * Say the number of calls in the conference */ public int getNumberOfCalls() { return conferenceManager.getNumberOfMembers(); } /** * Mute or unmute member in a whisperGroup */ public static void setMuteWhisperGroup(String callId, boolean isMuted) { if (callId == null) { return; } synchronized(activeCalls) { for (int i = 0; i < activeCalls.size(); i++) { CallHandler call = (CallHandler)activeCalls.elementAt(i); CallParticipant cp = call.getCallParticipant(); if (match(cp, callId)) { if (Logger.logLevel >= Logger.LOG_DETAIL) { String s = ""; if (isMuted == false) { s = "un"; } Logger.println(cp.getCallId() + ": " + s + "muted"); } MemberReceiver memberReceiver = call.getMemberReceiver(); if (memberReceiver != null) { memberReceiver.setMuteWhisperGroup(isMuted); } } } } } /** * Mute or unmute a conference from a particular call. */ public static void setConferenceMuted(String callId, boolean isMuted) { if (callId == null) { return; } synchronized(activeCalls) { for (int i = 0; i < activeCalls.size(); i++) { CallHandler call = (CallHandler)activeCalls.elementAt(i); CallParticipant cp = call.getCallParticipant(); if (match(cp, callId)) { if (Logger.logLevel >= Logger.LOG_DETAIL) { String s = ""; if (isMuted == false) { s = "un"; } Logger.println(cp.getCallId() + ": conference " + s + "muted"); } ConferenceMember member = call.getMember(); if (member!= null) { member.setConferenceMuted(isMuted); } } } } } /** * Mute or unmute the main conference from a particular call. */ public static void setConferenceSilenced(String callId, boolean isSilenced) { synchronized(activeCalls) { for (int i = 0; i < activeCalls.size(); i++) { CallHandler call = (CallHandler)activeCalls.elementAt(i); CallParticipant cp = call.getCallParticipant(); if (match(cp, callId)) { if (Logger.logLevel >= Logger.LOG_DETAIL) { String s = ""; if (isSilenced == false) { s = "un"; } Logger.println(cp.getCallId() + ": silenceMainonference " + s + "muted"); } ConferenceMember member = call.getMember(); if (member!= null) { member.setConferenceSilenced(isSilenced); } } } } } /* * Set powerThresholdLimit for the speech detector for a member. */ public static void setPowerThresholdLimit(String callId, double powerThresholdLimit) { synchronized(activeCalls) { for (int i = 0; i < activeCalls.size(); i++) { CallHandler call = (CallHandler)activeCalls.elementAt(i); CallParticipant cp = call.getCallParticipant(); if (match(cp, callId)) { MemberReceiver memberReceiver = call.getMemberReceiver(); if (memberReceiver != null) { memberReceiver.setPowerThresholdLimit( powerThresholdLimit); } } } } } /** * set dmtfSuppression flag */ private static boolean dtmfSuppression = true; public static void setDtmfSuppression(String callId, boolean dtmfSuppression) throws NoSuchElementException { if (callId.equals("0")) { CallHandler.dtmfSuppression = dtmfSuppression; return; } CallHandler callHandler = findCall(callId); if (callHandler == null) { throw new NoSuchElementException("Invalid callId specified: " + callId); } callHandler.getCallParticipant().setDtmfSuppression(dtmfSuppression); } /** * Set flag to do voice detection while muted */ public static void setVoiceDetectionWhileMuted(String callId, boolean voiceDetectionWhileMuted) { if (callId == null) { return; } synchronized(activeCalls) { for (int i = 0; i < activeCalls.size(); i++) { CallHandler call = (CallHandler)activeCalls.elementAt(i); CallParticipant cp = call.getCallParticipant(); if (match(cp, callId)) { cp.setVoiceDetectionWhileMuted(voiceDetectionWhileMuted); if (Logger.logLevel >= Logger.LOG_DETAIL) { Logger.println(cp.getCallId() + " voice detection while muted is " + voiceDetectionWhileMuted); } } } } } /** * Get global dtmfSuppression flag */ public static boolean dtmfSuppression() { return dtmfSuppression; } public static void setDoNotRecord(String callId, boolean doNotRecord) throws NoSuchElementException { CallHandler callHandler = findCall(callId); if (callHandler == null) { throw new NoSuchElementException("Invalid callId specified: " + callId); } if (Logger.logLevel >= Logger.LOG_DETAIL) { String s = ""; if (doNotRecord == true) { s = "NOT"; } Logger.println(callHandler + ": " + s + " okay to record"); } callHandler.getMemberReceiver().setDoNotRecord(doNotRecord); } /** * Record data sent to or from a member */ public static void recordMember(String callId, boolean enabled, String recordingFile, String recordingType, boolean fromMember) throws NoSuchElementException, IOException { CallHandler callHandler = findCall(callId); if (callHandler == null) { throw new NoSuchElementException("Invalid callId specified: " + callId); } if (fromMember) { callHandler.getMemberReceiver().setRecordFromMember(enabled, recordingFile, recordingType); } else { callHandler.getMemberSender().setRecordToMember(enabled, recordingFile, recordingType); } } /** * Play a treament to a member */ public static void playTreatmentToCall(String callId, String treatment) throws NoSuchElementException, IOException { playTreatmentToCall(callId, treatment, (TreatmentDoneListener) null); } public static void playTreatmentToCall(String callId, String treatment, double[] volume) throws NoSuchElementException, IOException { } public static void playTreatmentToCall(String callId, String treatment, TreatmentDoneListener treatmentDoneListener) throws NoSuchElementException, IOException { CallHandler callHandler = findCall(callId); if (callHandler == null) { throw new NoSuchElementException("Invalid callId specified: " + callId); } if (callHandler.isCallEstablished() == false) { throw new IOException("Call is not ESTABLISHED: " + callId); } callHandler.playTreatmentToCall(treatment, treatmentDoneListener); } public TreatmentManager playTreatmentToCall(String treatment) throws IOException { return playTreatmentToCall(treatment, (TreatmentDoneListener) null); } public TreatmentManager playTreatmentToCall(String treatment, TreatmentDoneListener treatmentDoneListener) throws IOException { if (Logger.logLevel >= Logger.LOG_MOREINFO) { Logger.println("Playing treatment " + treatment + " to " + cp.getCallId()); } TreatmentManager treatmentManager = new TreatmentManager(treatment, 0, conferenceManager.getMediaInfo().getSampleRate(), conferenceManager.getMediaInfo().getChannels()); if (treatmentDoneListener != null) { treatmentManager.addTreatmentDoneListener(treatmentDoneListener); } addTreatment(treatmentManager); return treatmentManager; } /** * get the IP address and port used to receive packets for this call. */ public InetSocketAddress getReceiveAddress() { return memberReceiver.getReceiveAddress(); } /** * get the IP address and port used to send packets to this call. */ public InetSocketAddress getSendAddress() { return memberSender.getSendAddress(); } /** * Determine if this is the first member to join the conference. * This is called to determine if a special audio treatment * should be played. */ public boolean isFirstMember() { return conferenceManager.isFirstMember(); } /** * For debugging... */ public static boolean tooManyDuplicateCalls(String phoneNumber) { synchronized(activeCalls) { int n = 0; for (int i = 0; i < activeCalls.size(); i++) { CallHandler call = (CallHandler)activeCalls.elementAt(i); CallParticipant cp = call.getCallParticipant(); if (cp.getPhoneNumber().equals(phoneNumber)) { n++; } } if (n > duplicateCallLimit) { return true; } return false; } } public static void setDuplicateCallLimit(int duplicateCallLimit) { CallHandler.duplicateCallLimit = duplicateCallLimit; } public static int getDuplicateCallLimit() { return duplicateCallLimit; } public static void enablePSTNCalls(boolean enablePSTNCalls) { CallHandler.enablePSTNCalls = enablePSTNCalls; } public static boolean enablePSTNCalls() { return enablePSTNCalls; } /** * String representation of this Caller * @return the string representation of this Caller */ public String toString() { return cp.toString(); } }