/** * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ * * Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below). * * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software * Foundation; either version 3.0 of the License, or (at your option) any later * version. * * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along * with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. * */ package org.bigbluebutton.voiceconf.sip; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.zoolu.sip.call.*; import org.zoolu.sip.provider.SipProvider; import org.zoolu.sip.provider.SipStack; import org.zoolu.sip.message.*; import org.zoolu.sdp.*; import org.bigbluebutton.voiceconf.messaging.IMessagingService; import org.bigbluebutton.voiceconf.red5.CallStreamFactory; import org.bigbluebutton.voiceconf.red5.ClientConnectionManager; import org.bigbluebutton.voiceconf.red5.media.CallStream; import org.bigbluebutton.voiceconf.red5.media.CallStreamObserver; import org.bigbluebutton.voiceconf.red5.media.StreamException; import org.bigbluebutton.voiceconf.util.StackTraceUtil; import org.red5.app.sip.codecs.Codec; import org.red5.app.sip.codecs.CodecUtils; import org.slf4j.Logger; import org.red5.logging.Red5LoggerFactory; import org.red5.server.api.scope.IScope; import org.red5.server.api.stream.IBroadcastStream; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.net.UnknownHostException; import java.util.Vector; public class CallAgent extends CallListenerAdapter implements CallStreamObserver { private static Logger log = Red5LoggerFactory.getLogger(CallAgent.class, "sip"); private final SipPeerProfile userProfile; private final SipProvider sipProvider; private final String clientRtpIp; private ExtendedCall call; private CallStream callStream; private String localSession = null; private Codec sipCodec = null; private CallStreamFactory callStreamFactory; private ClientConnectionManager clientConnManager; private final String clientId; private final AudioConferenceProvider portProvider; private DatagramSocket localSocket = null; private String _callerName; private String _destination; private Boolean listeningToGlobal = false; private IMessagingService messagingService; private enum CallState { UA_IDLE(0), UA_INCOMING_CALL(1), UA_OUTGOING_CALL(2), UA_ONCALL(3); private final int state; CallState(int state) {this.state = state;} private int getState() {return state;} } private CallState callState; public String getDestination() { return _destination; } public CallAgent(String sipClientRtpIp, SipProvider sipProvider, SipPeerProfile userProfile, AudioConferenceProvider portProvider, String clientId, IMessagingService messagingService) { this.sipProvider = sipProvider; this.clientRtpIp = sipClientRtpIp; this.userProfile = userProfile; this.portProvider = portProvider; this.clientId = clientId; this.messagingService = messagingService; } public String getCallId() { return clientId; } private void initSessionDescriptor() { log.debug("initSessionDescriptor"); SessionDescriptor newSdp = SdpUtils.createInitialSdp(userProfile.username, this.clientRtpIp, userProfile.audioPort, userProfile.videoPort, userProfile.audioCodecsPrecedence ); localSession = newSdp.toString(); log.debug("localSession Descriptor = " + localSession ); } public Boolean isListeningToGlobal() { return listeningToGlobal; } public void call(String callerName, String destination) { _callerName = callerName; _destination = destination; log.debug("{} making a call to {}", callerName, destination); try { localSocket = getLocalAudioSocket(); userProfile.audioPort = localSocket.getLocalPort(); } catch (Exception e) { log.debug("{} failed to allocate local port for call to {}. Notifying client that call failed.", callerName, destination); notifyListenersOnOutgoingCallFailed(); return; } setupCallerDisplayName(callerName, destination); userProfile.initContactAddress(sipProvider); initSessionDescriptor(); callState = CallState.UA_OUTGOING_CALL; call = new ExtendedCall(sipProvider, userProfile.fromUrl, userProfile.contactUrl, userProfile.username, userProfile.realm, userProfile.passwd, this); // In case of incomplete url (e.g. only 'user' is present), // try to complete it. destination = sipProvider.completeNameAddress(destination).toString(); log.debug("call {}", destination); if (userProfile.noOffer) { call.call(destination); } else { call.call(destination, localSession); } } private void setupCallerDisplayName(String callerName, String destination) { String fromURL = "\"" + callerName + "\" <sip:" + destination + "@" + portProvider.getHost() + ">"; userProfile.username = callerName; userProfile.fromUrl = fromURL; userProfile.contactUrl = "sip:" + destination + "@" + sipProvider.getViaAddress(); if (sipProvider.getPort() != SipStack.default_port) { userProfile.contactUrl += ":" + sipProvider.getPort(); } } /** Closes an ongoing, incoming, or pending call */ public void hangup() { if (callState == CallState.UA_IDLE) return; log.debug("hangup"); if (listeningToGlobal) { log.debug("Hanging up of a call connected to the global audio stream"); notifyListenersOfOnCallClosed(); } else { closeVoiceStreams(); if (call != null) call.hangup(); } callState = CallState.UA_IDLE; } private DatagramSocket getLocalAudioSocket() throws Exception { DatagramSocket socket = null; boolean failedToGetSocket = true; StringBuilder failedPorts = new StringBuilder("Failed ports: "); for (int i = portProvider.getStartAudioPort(); i <= portProvider.getStopAudioPort(); i++) { int freePort = portProvider.getFreeAudioPort(); try { socket = new DatagramSocket(freePort); failedToGetSocket = false; log.info("Successfully setup local audio port {}. {}", freePort, failedPorts); break; } catch (SocketException e) { failedPorts.append(freePort + ", "); } } if (failedToGetSocket) { log.warn("Failed to setup local audio port {}.", failedPorts); throw new Exception("Exception while initializing CallStream"); } return socket; } private boolean isGlobalAudioStream() { return (_callerName != null && _callerName.startsWith("GLOBAL_AUDIO_")); } private void createVoiceStreams() { if (callStream != null) { log.debug("Media application is already running."); return; } SessionDescriptor localSdp = new SessionDescriptor(call.getLocalSessionDescriptor()); SessionDescriptor remoteSdp = new SessionDescriptor(call.getRemoteSessionDescriptor()); String remoteMediaAddress = SessionDescriptorUtil.getRemoteMediaAddress(remoteSdp); int remoteAudioPort = SessionDescriptorUtil.getRemoteAudioPort(remoteSdp); int localAudioPort = SessionDescriptorUtil.getLocalAudioPort(localSdp); SipConnectInfo connInfo = new SipConnectInfo(localSocket, remoteMediaAddress, remoteAudioPort); try { localSocket.connect(InetAddress.getByName(remoteMediaAddress), remoteAudioPort); log.debug("[localAudioPort=" + localAudioPort + ",remoteAudioPort=" + remoteAudioPort + "]"); if (userProfile.audio && localAudioPort != 0 && remoteAudioPort != 0) { if ((callStream == null) && (sipCodec != null)) { try { callStream = callStreamFactory.createCallStream(sipCodec, connInfo); callStream.addCallStreamObserver(this); callStream.start(); if (isGlobalAudioStream()) { GlobalCall.addGlobalAudioStream(_destination, callStream.getListenStreamName(), sipCodec, connInfo); } else { notifyListenersOnCallConnected(callStream.getTalkStreamName(), callStream.getListenStreamName()); } } catch (Exception e) { log.error("Failed to create Call Stream."); System.out.println(StackTraceUtil.getStackTrace(e)); } } } } catch (UnknownHostException e1) { log.error(StackTraceUtil.getStackTrace(e1)); } } public void startTalkStream(IBroadcastStream broadcastStream, IScope scope) { try { callStream.startTalkStream(broadcastStream, scope); } catch (StreamException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void stopTalkStream(IBroadcastStream broadcastStream, IScope scope) { if (callStream != null) { callStream.stopTalkStream(broadcastStream, scope); } else { log.info("Can't stop talk stream as stream may have already stopped."); } } public void connectToGlobalStream(String clientId, String callerIdName, String voiceConf) { listeningToGlobal = true; _destination = voiceConf; String globalAudioStreamName = GlobalCall.getGlobalAudioStream(voiceConf); while (globalAudioStreamName.equals("reserved")) { try { Thread.sleep(100); } catch (Exception e) { } globalAudioStreamName = GlobalCall.getGlobalAudioStream(voiceConf); } GlobalCall.addUser(clientId, callerIdName, _destination); sipCodec = GlobalCall.getRoomCodec(voiceConf); callState = CallState.UA_ONCALL; notifyListenersOnCallConnected("", globalAudioStreamName); log.info("User is has connected to global audio, user=[" + callerIdName + "] voiceConf = [" + voiceConf + "]"); messagingService.userConnectedToGlobalAudio(voiceConf, callerIdName); } private void closeVoiceStreams() { log.debug("Shutting down the voice streams."); if (callStream != null) { callStream.stop(); callStream = null; } else { log.debug("Can't shutdown voice stream. callstream is NULL"); } } // ********************** Call callback functions ********************** private void createAudioCodec(SessionDescriptor newSdp) { sipCodec = SdpUtils.getNegotiatedAudioCodec(newSdp); } private void setupSdpAndCodec(String sdp) { SessionDescriptor remoteSdp = new SessionDescriptor(sdp); SessionDescriptor localSdp = new SessionDescriptor(localSession); log.debug("localSdp = " + localSdp.toString() + "."); log.debug("remoteSdp = " + remoteSdp.toString() + "."); // First we need to make payloads negotiation so the related attributes can be then matched. SessionDescriptor newSdp = SdpUtils.makeMediaPayloadsNegotiation(localSdp, remoteSdp); createAudioCodec(newSdp); // Now we complete the SDP negotiation informing the selected // codec, so it can be internally updated during the process. SdpUtils.completeSdpNegotiation(newSdp, localSdp, remoteSdp); localSession = newSdp.toString(); log.debug("newSdp = " + localSession + "." ); // Finally, we use the "newSdp" and "remoteSdp" to initialize the lasting codec informations. CodecUtils.initSipAudioCodec(sipCodec, userProfile.audioDefaultPacketization, userProfile.audioDefaultPacketization, newSdp, remoteSdp); } /** Callback function called when arriving a 2xx (call accepted) * The user has managed to join the conference. */ public void onCallAccepted(Call call, String sdp, Message resp) { log.debug("Received 200/OK. So user has successfully joined the conference."); if (!isCurrentCall(call)) return; callState = CallState.UA_ONCALL; setupSdpAndCodec(sdp); if (userProfile.noOffer) { // Answer with the local sdp. call.ackWithAnswer(localSession); } createVoiceStreams(); } /** Callback function called when arriving an ACK method (call confirmed) */ public void onCallConfirmed(Call call, String sdp, Message ack) { log.debug("Received ACK. Hmmm...is this for when the server initiates the call????"); if (!isCurrentCall(call)) return; callState = CallState.UA_ONCALL; createVoiceStreams(); } /** Callback function called when arriving a 4xx (call failure) */ public void onCallRefused(Call call, String reason, Message resp) { log.debug("Call has been refused."); if (!isCurrentCall(call)) return; callState = CallState.UA_IDLE; notifyListenersOnOutgoingCallFailed(); } /** Callback function called when arriving a 3xx (call redirection) */ public void onCallRedirection(Call call, String reason, Vector contact_list, Message resp) { log.debug("onCallRedirection"); if (!isCurrentCall(call)) return; call.call(((String) contact_list.elementAt(0))); } /** * Callback function that may be overloaded (extended). Called when arriving a CANCEL request */ public void onCallCanceling(Call call, Message cancel) { log.error("Server shouldn't cancel call...or does it???"); if (!isCurrentCall(call)) return; log.debug("Server has CANCEL-led the call."); callState = CallState.UA_IDLE; notifyListenersOfOnIncomingCallCancelled(); } private void notifyListenersOnCallConnected(String talkStream, String listenStream) { log.debug("notifyListenersOnCallConnected for {}", clientId); clientConnManager.joinConferenceSuccess(clientId, talkStream, listenStream, sipCodec.getCodecName()); } private void notifyListenersOnOutgoingCallFailed() { log.debug("notifyListenersOnOutgoingCallFailed for {}", clientId); clientConnManager.joinConferenceFailed(clientId); cleanup(); } private void notifyListenersOfOnIncomingCallCancelled() { log.debug("notifyListenersOfOnIncomingCallCancelled for {}", clientId); } private void notifyListenersOfOnCallClosed() { if (callState == CallState.UA_IDLE) return; log.debug("notifyListenersOfOnCallClosed for {}", clientId); clientConnManager.leaveConference(clientId); cleanup(); } private void cleanup() { if (localSocket == null) return; log.debug("Closing local audio port {}", localSocket.getLocalPort()); if (!listeningToGlobal) { localSocket.close(); } } /** Callback function called when arriving a BYE request */ public void onCallClosing(Call call, Message bye) { log.info("Received a BYE from the other end telling us to hangup."); if (!isCurrentCall(call)) return; closeVoiceStreams(); notifyListenersOfOnCallClosed(); callState = CallState.UA_IDLE; // Reset local sdp for next call. initSessionDescriptor(); } /** * Callback function called when arriving a response after a BYE request * (call closed) */ public void onCallClosed(Call call, Message resp) { log.debug("onCallClosed"); if (!isCurrentCall(call)) return; log.debug("CLOSE/OK."); notifyListenersOfOnCallClosed(); callState = CallState.UA_IDLE; } /** Callback function called when the invite expires */ public void onCallTimeout(Call call) { log.debug("onCallTimeout"); if (!isCurrentCall(call)) return; log.debug("NOT FOUND/TIMEOUT."); callState = CallState.UA_IDLE; notifyListenersOnOutgoingCallFailed(); } public void onCallStreamStopped() { log.info("Call stream has been stopped"); notifyListenersOfOnCallClosed(); } private boolean isCurrentCall(Call call) { return this.call == call; } public void setCallStreamFactory(CallStreamFactory csf) { this.callStreamFactory = csf; } public void setClientConnectionManager(ClientConnectionManager ccm) { clientConnManager = ccm; } }