package org.red5.app.sip; import org.zoolu.sip.call.*; import org.zoolu.sip.address.*; import org.zoolu.sip.provider.SipProvider; import org.zoolu.sip.header.StatusLine; import org.zoolu.sip.message.*; import org.zoolu.sdp.*; 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 org.zoolu.tools.Parser; import java.util.Enumeration; import java.util.HashSet; import java.util.Set; import java.util.Vector; public class SipUserAgent extends CallListenerAdapter { private static Logger log = Red5LoggerFactory.getLogger(SipUserAgent.class, "sip"); private SipUserAgentProfile userProfile; private SipProvider sipProvider; private ExtendedCall call; private ExtendedCall callTransfer; private CallStream callStream; private String localSession = null; private Codec sipCodec = null; private final ScopeProvider scopeProvider; private Set<SipUserAgentListener> listeners = new HashSet<SipUserAgentListener>(); 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 SipUserAgent(SipProvider sipProvider, SipUserAgentProfile userProfile, ScopeProvider scopeProvider) { this.scopeProvider = scopeProvider; this.sipProvider = sipProvider; this.userProfile = userProfile; // If no contact_url and/or from_url has been set, create it now. userProfile.initContactAddress(sipProvider); // Set local sdp. initSessionDescriptor(); } public void addListener(SipUserAgentListener listener) { listeners.add(listener); } public void removeListener(SipUserAgentListener listener) { listeners.remove(listener); } private void changeStatus(CallState state) { callState = state; } public boolean isIdle() { return callState == CallState.UA_IDLE; } public void queueSipDtmfDigits(String digits) { callStream.queueSipDtmfDigits(digits); } public void initialize() { waitForIncomingCalls(); } public void initSessionDescriptor() { log.debug("initSessionDescriptor"); SessionDescriptor newSdp = SdpUtils.createInitialSdp(userProfile.username, sipProvider.getViaAddress(), userProfile.audioPort, userProfile.videoPort, userProfile.audioCodecsPrecedence ); localSession = newSdp.toString(); log.debug("localSession Descriptor = " + localSession ); } public void call(String targetUrl) { log.debug( "call", "Init..." ); changeStatus(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. targetUrl = sipProvider.completeNameAddress(targetUrl).toString(); if (userProfile.noOffer) { call.call(targetUrl); } else { call.call(targetUrl, localSession); } } /** Call Transfer test by Lior */ public void transfer( String transferTo ){ log.debug("REFER/TRANSFER", "Init..." ); if (call != null && call.isOnCall()) { call.transfer(transferTo); } } /** end of transfer test code */ /** Waits for an incoming call (acting as UAS). */ private void waitForIncomingCalls() { log.debug("waitForIncomingCalls..." ); changeStatus(CallState.UA_IDLE); call = new ExtendedCall(sipProvider, userProfile.fromUrl, userProfile.contactUrl, userProfile.username, userProfile.realm, userProfile.passwd, this); call.listen(); } /** Closes an ongoing, incoming, or pending call */ public void hangup() { log.debug("hangup"); if (isIdle()) return; closeMediaApplication(); if (call != null) call.hangup(); changeStatus(CallState.UA_IDLE); waitForIncomingCalls(); } /** Closes an ongoing, incoming, or pending call */ public void accept() { log.debug("accept", "Init..."); if (call != null) { call.accept(localSession); } } public void redirect(String redirection) { log.debug( "redirect", "Init..." ); if (call != null) { call.redirect(redirection); } } protected void launchMediaApplication() { // Exit if the Media Application is already running. if (callStream != null) { log.debug("launchMediaApplication", "Media application is already running."); return; } SessionDescriptor localSdp = new SessionDescriptor( call.getLocalSessionDescriptor() ); SessionDescriptor remoteSdp = new SessionDescriptor( call.getRemoteSessionDescriptor() ); String remoteMediaAddress = (new Parser(remoteSdp.getConnection().toString())).skipString().skipString().getString(); int remoteAudioPort = getRemoteAudioPort(remoteSdp); int localAudioPort = getLocalAudioPort(localSdp); log.debug("[localAudioPort=" + localAudioPort + ",remoteAudioPort=" + remoteAudioPort + "]"); if (userProfile.audio && localAudioPort != 0 && remoteAudioPort != 0) { if ((callStream == null) && (sipCodec != null)) { SipConnectInfo connInfo = new SipConnectInfo(localAudioPort, remoteMediaAddress, remoteAudioPort); try { callStream = new CallStream(sipCodec, connInfo, scopeProvider); notifyListenersOnCallConnected(callStream.getTalkStreamName(), callStream.getListenStreamName()); } catch (Exception e) { log.error("Failed to create Call Stream."); } } } } private void notifyListenersOnCallConnected(String talkStream, String listenStream) { for (SipUserAgentListener listener : listeners) { listener.onCallConnected(talkStream, listenStream); } } private int getLocalAudioPort(SessionDescriptor localSdp) { int localAudioPort = 0; //int localVideoPort = 0; // parse local sdp for ( Enumeration e = localSdp.getMediaDescriptors().elements(); e.hasMoreElements(); ) { MediaField media = ( (MediaDescriptor) e.nextElement() ).getMedia(); if ( media.getMedia().equals( "audio" ) ) { localAudioPort = media.getPort(); } //if ( media.getMedia().equals( "video" ) ) { // localVideoPort = media.getPort(); //} } return localAudioPort; } private int getRemoteAudioPort(SessionDescriptor remoteSdp) { int remoteAudioPort = 0; //int remoteVideoPort = 0; for (Enumeration e = remoteSdp.getMediaDescriptors().elements(); e.hasMoreElements();) { MediaDescriptor descriptor = (MediaDescriptor) e.nextElement(); MediaField media = descriptor.getMedia(); if ( media.getMedia().equals( "audio" ) ) { remoteAudioPort = media.getPort(); } // if ( media.getMedia().equals( "video" ) ) { // remoteVideoPort = media.getPort(); // } } return remoteAudioPort; } public void startTalkStream(IBroadcastStream broadcastStream, IScope scope) { callStream.startTalkStream(broadcastStream, scope); } public void stopTalkStream(IBroadcastStream broadcastStream, IScope scope) { if (callStream != null) { callStream.stopTalkStream(broadcastStream, scope); } } private void closeMediaApplication() { log.debug("closeMediaApplication" ); if (callStream != null) { callStream.stopMedia(); callStream = null; } } // ********************** Call callback functions ********************** private void createAudioCodec(SessionDescriptor newSdp) { sipCodec = SdpUtils.getNegotiatedAudioCodec(newSdp); } /** * Callback function called when arriving a new INVITE method (incoming call) */ public void onCallIncoming(Call call, NameAddress callee, NameAddress caller, String sdp, Message invite) { log.debug("onCallIncoming"); if (!isCurrentCall(call)) return; log.debug("IncomingCallIncoming()"); changeStatus(CallState.UA_INCOMING_CALL); call.ring(); if (sdp != null) { setupSdpAndCodec(sdp); } notifyListenersOfNewIncomingCall(callee, caller); } 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); } private void notifyListenersOfNewIncomingCall(NameAddress callee, NameAddress caller) { String source = caller.getAddress().toString(); String sourceName = caller.hasDisplayName() ? caller.getDisplayName() : ""; String destination = callee.getAddress().toString(); String destinationName = callee.hasDisplayName() ? callee.getDisplayName() : ""; for (SipUserAgentListener listener : listeners) { listener.onNewIncomingCall(source, sourceName, destination, destinationName); } } /** * Callback function called when arriving a new Re-INVITE method (re-inviting/call modify) */ public void onCallModifying(Call call, String sdp, Message invite) { log.debug("onCallModifying"); if (!isCurrentCall(call)) return; log.debug("RE-INVITE/MODIFY."); // to be implemented. // currently it simply accepts the session changes (see method // onCallModifying() in CallListenerAdapter) super.onCallModifying(call, sdp, invite); } /** * Callback function that may be overloaded (extended). Called when arriving a 180 Ringing */ public void onCallRinging(Call call, Message resp) { log.debug("onCallRinging"); if (!isCurrentCallOrCallTransfer(call)) return; log.debug("RINGING." ); notifyListenersOfOnOutgoingCallRemoteRinging(); } private void notifyListenersOfOnOutgoingCallRemoteRinging() { for (SipUserAgentListener listener : listeners) { listener.onOutgoingCallRemoteRinging(); } } /** Callback function called when arriving a 2xx (call accepted) */ public void onCallAccepted(Call call, String sdp, Message resp) { log.debug( "onCallAccepted"); if (!isCurrentCallOrCallTransfer(call)) return; log.debug("ACCEPTED/CALL."); changeStatus(CallState.UA_ONCALL); setupSdpAndCodec(sdp); if (userProfile.noOffer) { // Answer with the local sdp. call.ackWithAnswer(localSession); } launchMediaApplication(); if (call == callTransfer) { StatusLine statusLine = resp.getStatusLine(); int code = statusLine.getCode(); String reason = statusLine.getReason(); this.call.notify(code, reason); } notifyListenersOfOnOutgoingCallAccepted(); } public void notifyListenersOfOnOutgoingCallAccepted() { for (SipUserAgentListener listener : listeners) { listener.onOutgoingCallAccepted(); } } /** Callback function called when arriving an ACK method (call confirmed) */ public void onCallConfirmed(Call call, String sdp, Message ack) { log.debug("onCallConfirmed"); if (!isCurrentCall(call)) return; log.debug("CONFIRMED/CALL."); changeStatus(CallState.UA_ONCALL); launchMediaApplication(); } /** Callback function called when arriving a 2xx (re-invite/modify accepted) */ public void onCallReInviteAccepted(Call call, String sdp, Message resp) { log.debug( "onCallReInviteAccepted"); if (!isCurrentCall(call)) return; log.debug("RE-INVITE-ACCEPTED/CALL." ); } /** Callback function called when arriving a 4xx (re-invite/modify failure) */ public void onCallReInviteRefused(Call call, String reason, Message resp) { log.debug("onCallReInviteRefused"); if (!isCurrentCall(call)) return; log.debug("RE-INVITE-REFUSED (" + reason + ")/CALL."); notifyListenersOnOutgoingCallFailed(); waitForIncomingCalls(); } /** Callback function called when arriving a 4xx (call failure) */ public void onCallRefused(Call call, String reason, Message resp) { log.debug("onCallRefused"); if (!isCurrentCall(call)) return; log.debug("REFUSED (" + reason + ")."); changeStatus(CallState.UA_IDLE); if (call == callTransfer) { StatusLine status_line = resp.getStatusLine(); int code = status_line.getCode(); // String reason=status_line.getReason(); this.call.notify(code, reason); callTransfer = null; } notifyListenersOnOutgoingCallFailed(); waitForIncomingCalls(); } private void notifyListenersOnOutgoingCallFailed() { for (SipUserAgentListener listener : listeners) { listener.onOutgoingCallFailed(); } } /** 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; log.debug("REDIRECTION (" + reason + ")." ); 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.debug("onCallCanceling"); if (!isCurrentCall(call)) return; log.debug("CANCEL."); changeStatus(CallState.UA_IDLE); notifyListenersOfOnIncomingCallCancelled(); waitForIncomingCalls(); } private void notifyListenersOfOnIncomingCallCancelled() { for (SipUserAgentListener listener : listeners) { listener.onIncomingCallCancelled(); } } /** Callback function called when arriving a BYE request */ public void onCallClosing(Call call, Message bye) { log.debug("onCallClosing"); if (!isCurrentCallOrCallTransfer(call)) return; if (call != callTransfer && callTransfer != null) { log.debug("CLOSE PREVIOUS CALL."); this.call = callTransfer; callTransfer = null; return; } log.debug("CLOSE."); closeMediaApplication(); notifyListenersOfOnCallClosed(); changeStatus(CallState.UA_IDLE); // Reset local sdp for next call. initSessionDescriptor(); waitForIncomingCalls(); } private void notifyListenersOfOnCallClosed() { for (SipUserAgentListener listener : listeners) { listener.onCallClosed(); } } /** * 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(); changeStatus(CallState.UA_IDLE); waitForIncomingCalls(); } /** Callback function called when the invite expires */ public void onCallTimeout(Call call) { log.debug("onCallTimeout"); if (!isCurrentCall(call)) return; log.debug("NOT FOUND/TIMEOUT."); changeStatus(CallState.UA_IDLE); if (call == callTransfer) { int code = 408; String reason = "Request Timeout"; this.call.notify(code, reason); callTransfer = null; } notifyListenersOnOutgoingCallFailed(); waitForIncomingCalls(); } // ****************** ExtendedCall callback functions ****************** /** * Callback function called when arriving a new REFER method (transfer request) */ public void onCallTransfer(ExtendedCall call, NameAddress refer_to, NameAddress refered_by, Message refer) { log.debug("onCallTransfer"); if (!isCurrentCall(call)) return; log.debug("Transfer to " + refer_to.toString() + "."); call.acceptTransfer(); callTransfer = new ExtendedCall(sipProvider, userProfile.fromUrl, userProfile.contactUrl, this); callTransfer.call(refer_to.toString(), localSession); } /** Callback function called when a call transfer is accepted. */ public void onCallTransferAccepted(ExtendedCall call, Message resp) { log.debug("onCallTransferAccepted"); if (!isCurrentCall(call)) return; log.debug("Transfer accepted."); } /** Callback function called when a call transfer is refused. */ public void onCallTransferRefused(ExtendedCall call, String reason, Message resp) { log.debug("onCallTransferRefused"); if (!isCurrentCall(call)) return; log.debug("Transfer refused."); } /** Callback function called when a call transfer is successfully completed */ public void onCallTransferSuccess(ExtendedCall call, Message notify) { log.debug("onCallTransferSuccess"); if (!isCurrentCall(call)) return; log.debug("Transfer succeeded."); call.hangup(); notifyListenersOfOnCallTransferred(); } private void notifyListenersOfOnCallTransferred() { for (SipUserAgentListener listener : listeners) { listener.onCallTrasferred(); } } /** * Callback function called when a call transfer is NOT successfully completed */ public void onCallTransferFailure(ExtendedCall call, String reason, Message notify) { log.debug("onCallTransferFailure"); if (!isCurrentCall(call)) return; log.info("Transfer failed."); } private boolean isCurrentCallOrCallTransfer(Call call) { return (call == this.call) || (call != callTransfer); } private boolean isCurrentCall(Call call) { return this.call == call; } }