/* * 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.CallState; import com.sun.voip.CallEvent; import com.sun.voip.CallEventListener; import com.sun.voip.Logger; import com.sun.voip.TreatmentManager; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketException; import java.util.ArrayList; import org.voicebridge.Application; /** * Initiate a call, and join a conference * * This is a separate thread so that it monitor the call status. * * This class handles a single party (joining a conference) * as well as two party calls. * * There is also support to try alternate gateways if one * gateway can't handle a new call. */ public class OutgoingCallHandler extends CallHandler implements CallEventListener { private CallEventListener csl; private Integer callInitiatedLock = new Integer(0); private Integer stateChangeLock = new Integer(0); private Integer waitCallAnswerLock = new Integer(0); private Integer waitCallEstablishedLock = new Integer(0); private boolean lastGateway = false; private boolean onlyOneGateway = false; public OutgoingCallHandler(CallEventListener callEventListener, CallParticipant cp) { addCallEventListener(this); csl = callEventListener; this.cp = cp; setName("Outgoing CallHandler for " + cp); } public CallEventListener getRequestHandler() { return csl; } /* * Thread to start a new call and join a conference. */ private static int nCalls = 0; // for debugging two gateways public void run() { /* * Join an existing conference or create a new one. */ synchronized (ConferenceManager.getConferenceList()) { conferenceManager = ConferenceManager.getConference(cp); if (conferenceManager == null) { Logger.error("Couldn't start conference " + cp.getConferenceId()); sendCallEventNotification( new CallEvent(CallEvent.CANT_START_CONFERENCE)); return; } try { member = conferenceManager.joinConference(cp); memberSender = member.getMemberSender(); memberReceiver = member.getMemberReceiver(); } catch (IOException e) { CallEvent callEvent = new CallEvent(CallEvent.CANT_CREATE_MEMBER); callEvent.setInfo(e.getMessage()); sendCallEventNotification(callEvent); removeCallEventListener(this); return; } } addCall(this); // add to list of active calls lastGateway = false; onlyOneGateway = false; /* * Start the call (INVITE) and wait for it to end (BYE). */ ArrayList voIPGateways = SipServer.getVoIPGateways(); String gateway = cp.getVoIPGateway(); if (gateway != null) { /* * User specified a specific gateway. Use that one only. */ Logger.println("Call " + this + ": Using gateway specified for the call: " + gateway); lastGateway = true; onlyOneGateway = true; placeCall(); } else if (voIPGateways.size() > 0) { if (voIPGateways.size() == 1) { onlyOneGateway = true; } lastGateway = true; placeCall(); } else if (cp.getPhoneNumber() != null && cp.getPhoneNumber().indexOf("sip:") == 0) { placeCall(); // no gateway involved, direct SIP call } else if (cp.getProtocol() != null && ("Speaker".equals(cp.getProtocol()) || "WebRtc".equals(cp.getProtocol()) || "Rtmfp".equals(cp.getProtocol()))) { placeCall(); // WebRtc call } else { Logger.error("Couldn't place call " + cp); sendCallEventNotification( new CallEvent(CallEvent.CANT_START_CONFERENCE)); } conferenceManager.leave(member); // Remove member from conference. removeCall(this); // remove call from active call list removeCallEventListener(this); done = true; } private void placeCall() { String protocol = Bridge.getDefaultProtocol(); if (cp.getProtocol() != null) { protocol = cp.getProtocol(); } if (protocol.equalsIgnoreCase("SIP")) { csa = new SipTPCCallAgent(this); } else if (protocol.equalsIgnoreCase("NS")) { csa = new NSOutgoingCallAgent(this); } else if (protocol.equalsIgnoreCase("WebRtc")) { csa = new WebRtcCallAgent(this); } else if (protocol.equalsIgnoreCase("Speaker")) { csa = new SpeakerCallAgent(this); } else if (protocol.equalsIgnoreCase("Rtmfp")) { csa = new RtmfpCallAgent(this); } else { //csa = new H323TPCCallAgent(this); reasonCallEnded = CallEvent.getEventString(CallEvent.H323_NOT_IMPLEMENTED); sendCallEventNotification( new CallEvent(CallEvent.H323_NOT_IMPLEMENTED)); Logger.println("Call " + cp + ": " + reasonCallEnded); return; } try { csa.initiateCall(); synchronized (callInitiatedLock) { callInitiatedLock.notifyAll(); } synchronized(stateChangeLock) { if (reasonCallEnded == null) { //if (protocol.equalsIgnoreCase("SIP") == false) { // /* // * Leave Conference and rejoin with the right local media parameters // * XXX Need to somehow get the socket from the h323 stack! // */ // member.getMemberReceiver().setReceiveSocket(); // conferenceManager.transferMember(conferenceManager, member); //} try { stateChangeLock.wait(); // wait for call to end } catch (InterruptedException e) { } } } } catch (IOException e) { synchronized (callInitiatedLock) { callInitiatedLock.notifyAll(); } if (reasonCallEnded == null) { cancelRequest(e.getMessage()); } Logger.println("Call " + this + " Exception " + e.getMessage()); } } /* * This method is called where there is new status information. * Status can be a state change, dtmf key pressed, * or speaking not speaking notification. */ public void callEventNotification(CallEvent callEvent) { if (Logger.logLevel >= Logger.LOG_INFO) { Logger.println("Notification: " + callEvent); } if (callEvent.equals(CallEvent.STATE_CHANGED)) { if (callEvent.getCallState().equals(CallState.ANSWERED)) { /* * For two party calls */ synchronized(waitCallAnswerLock) { waitCallAnswerLock.notify(); } } else if (callEvent.getCallState().equals(CallState.ESTABLISHED)) { /* * For migrating calls */ synchronized(waitCallEstablishedLock) { waitCallEstablishedLock.notify(); } } else if (callEvent.getCallState().equals(CallState.ENDING)) { CallHandler callHandler = CallHandler.findMigratingCall(cp.getCallId()); if (callHandler == this) { /* * If it's a gateway error and it's not the last gateway, * don't end the call. It will be retried with the * alternate gateway. */ if (callEvent.getInfo().indexOf("gateway error") >= 0 && lastGateway == false) { return; } callEvent = new CallEvent(CallEvent.MIGRATION_FAILED); callEvent.setInfo("Migration failed: " + getReasonCallEnded()); sendCallEventNotification(callEvent); } } else if (callEvent.getCallState().equals(CallState.ENDED)) { reasonCallEnded = callEvent.getInfo(); synchronized(waitCallAnswerLock) { waitCallAnswerLock.notify(); } if (reasonCallEnded.indexOf("gateway error") >= 0 && lastGateway == false) { CallHandler callHandler = CallHandler.findMigratingCall(cp.getCallId()); if (callHandler == this) { synchronized(stateChangeLock) { /* * Let the outgoing call handler know so * it can try another gateway. */ stateChangeLock.notify(); } return; // don't tell the migrator yet } } synchronized(waitCallEstablishedLock) { waitCallEstablishedLock.notify(); } synchronized(stateChangeLock) { stateChangeLock.notify(); // the call has ended } /* * If it's a gateway error and not the last gateway, * don't end the call. It will be retried with the * alternate gateway. */ if (reasonCallEnded.indexOf("gateway error") >= 0 && lastGateway == false) { return; } cancelRequest(reasonCallEnded); } } if (suppressEvent(cp, callEvent) == false) { Application.outgoingCallNotification(callEvent); if (csl != null) csl.callEventNotification(callEvent); } } /* * 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) { // // member.initialize(this, isa, mediaPayload, receivePayload, telephoneEventPayload); //} /* * To make call migration and automatic retries to alternate gateways * transparent to the facilitator, we need to suppress certain * status messages. */ private boolean suppressEvent(CallParticipant cp, CallEvent callEvent) { /* * Suppress status from migrated calls so the facilitator * doesn't see CALL_ENDED from the previous call. * * XXX Not sure about this. I think we want the status to go through. * The receiver of the status will see "migrated" in the message * and can decide what to do. * * For the new call, we allow "No Answer". Once the call is answered * we clear the migrateCall flag so that CALL_ENDING and CALL_END * will be delivered to the client */ if (suppressStatus == true) { if (callEvent.getInfo() != null && callEvent.getInfo().indexOf("No Answer") >= 0 || callEvent.equals(CallEvent.BUSY_HERE) || callEvent.equals(CallEvent.CALL_ANSWER_TIMEOUT) || callEvent.equals(CallEvent.MIGRATED) || callEvent.equals(CallEvent.MIGRATION_FAILED) || callEvent.equals(CallEvent.JOIN_TIMEOUT)) { return false; } return true; } /* * We automatically retry calls with an alternate gateway * when there is a gateway error. The status sent to the * socket should make the switch to the alternate gateway transparent. * We don't want to send CALL_ENDING or CALL_ENDED until * we've tried the alternate gateway. * We also want to suppress CALL_PARTICIPANT_INVITED when * trying the alternate gateway. */ if (lastGateway == false) { /* * Suppress gateway errors from default gateway */ if (callEvent.getInfo().indexOf("gateway error") >= 0) { return true; } return false; } /* * Suppress CALL_PARTICIPANT_INVITED message from alternate gateway */ if (onlyOneGateway == false && callEvent.equals(CallEvent.STATE_CHANGED) && callEvent.getCallState().equals(CallState.INVITED)) { return true; } /* * No need to suppress this message. */ return false; } /* * terminate a call. */ public void cancelRequest(String reason) { done = true; if (csa != null) { CallHandler migratingCall = CallHandler.findMigratingCall(cp.getCallId()); if (migratingCall == this) { Logger.println("Failed to Migrate: " + reason); } csa.cancelRequest(reason); } synchronized(waitCallAnswerLock) { waitCallAnswerLock.notify(); } CallHandler otherCall = this.otherCall; this.otherCall = null; if (otherCall != null) { Logger.println("otherCall is " + otherCall.getCallParticipant()); otherCall.cancelRequest("Two party call ended"); } } public String getSdp() { synchronized (callInitiatedLock) { while (csa == null && !done && reasonCallEnded == null) { try { callInitiatedLock.wait(); } catch (InterruptedException e) { } } return csa.getSdp(); } } /* * For two party calls. * * When one party hangs up, the other call should be terminated as well. */ //private OutgoingCallHandler otherCall; public void setOtherCall(OutgoingCallHandler otherCall) { this.otherCall = otherCall; } /* * For two party calls, we wait until the first party answers * before calling the second party. * * When the first party answers, the second party is called and * the treatment is played to the first party. * * When the second party answers, the treatment to the first party * is stopped. */ public boolean waitForCallToBeAnswered() { String protocol = Bridge.getDefaultProtocol(); if (cp.getProtocol() != null) { protocol = cp.getProtocol(); } if (protocol.equalsIgnoreCase("WebRtc") || protocol.equalsIgnoreCase("Rtmfp") || protocol.equalsIgnoreCase("Speaker")) { return true; } synchronized(waitCallAnswerLock) { if (done || reasonCallEnded != null) { return false; } try { waitCallAnswerLock.wait(); } catch (InterruptedException e) { } } if (done || reasonCallEnded != null) { return false; } return true; } public boolean waitForCallToBeEstablished() { if (cp.getProtocol().equalsIgnoreCase("WebRtc") || cp.getProtocol().equalsIgnoreCase("Rtmfp") || cp.getProtocol().equalsIgnoreCase("Speaker")) { return true; } synchronized(waitCallEstablishedLock) { if (done || reasonCallEnded != null) { return false; } try { waitCallEstablishedLock.wait(); } catch (InterruptedException e) { } } if (done || reasonCallEnded != null) { return false; } return true; } /* * Cancel all calls started by the specified requestHandler */ public static void hangup(CallEventListener callEventListener, String reason) { ArrayList<CallHandler> callsToCancel = new ArrayList(); 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); if (call.getRequestHandler() == callEventListener) { callsToCancel.add(call); } } } cancel(callsToCancel, reason); } private static void cancel(ArrayList<CallHandler> callsToCancel, String reason) { while (callsToCancel.size() > 0) { CallHandler call = callsToCancel.remove(0); call.cancelRequest(reason); } } /** * String representation of this OutgoingCallHandler * @return the string representation of this OutgoingCallHandler */ public String toString() { return cp.toString(); } }