package com.voxeo.moho.sip; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Map; import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMultipart; import javax.media.mscontrol.networkconnection.NetworkConnection; import javax.media.mscontrol.networkconnection.SdpPortManagerEvent; import javax.sdp.MediaDescription; import javax.sdp.SdpFactory; import javax.sdp.SessionDescription; import javax.servlet.sip.Address; import javax.servlet.sip.SipApplicationSession; import javax.servlet.sip.SipServletRequest; import javax.servlet.sip.SipServletResponse; import javax.servlet.sip.SipSession; import org.apache.log4j.Logger; import com.voxeo.moho.SignalException; import com.voxeo.moho.event.CallCompleteEvent; import com.voxeo.moho.media.siprecord.metadata.Association; import com.voxeo.moho.media.siprecord.metadata.CommunicationSession; import com.voxeo.moho.media.siprecord.metadata.MediaStream; import com.voxeo.moho.media.siprecord.metadata.ParticipantMetadata; import com.voxeo.moho.media.siprecord.metadata.RecordingSession; import com.voxeo.moho.spi.ExecutionContext; import com.voxeo.moho.util.SDPUtils; // TODO SRS failover? public class SipRecordingCall extends SIPOutgoingCall { private static final Logger LOG = Logger.getLogger(SipRecordingCall.class); private String _label; private RecordingSession rsMetadata; private boolean pauseresumingRecording; private Object condition = new Object(); private Exception asyncException; public SipRecordingCall(ExecutionContext context, SIPEndpoint from, SIPEndpoint to) { super(context, from, to, null); } public String getLabel() { return _label; } public void setLabel(String _label) { this._label = _label; } public RecordingSession getRSMetadata() { return rsMetadata; } public void setRSMetadata(RecordingSession rsMetadata) { this.rsMetadata = rsMetadata; } @Override public synchronized void onEvent(SdpPortManagerEvent event) { if (pauseresumingRecording) { if (event.isSuccessful()) { pauseRecordingComplete(null); } else { // pause recording failed pauseRecordingComplete(new SignalException("Failed process SDP.")); } } else { super.onEvent(event); } } protected void pauseRecordingComplete(Exception ex) { if (ex != null) { LOG.error("Failed to pause/resume SIP recording", ex); asyncException = ex; } synchronized (condition) { pauseresumingRecording = false; condition.notifyAll(); } } @Override protected synchronized void doResponse(SipServletResponse res, Map<String, String> headers) throws Exception { if (pauseresumingRecording) { if (res.getStatus() >= 200 && res.getStatus() < 300) { try { res.createAck().send(); ((NetworkConnection) getMediaObject()).getSdpPortManager().processSdpOffer(res.getRawContent()); } catch (Exception ex) { pauseRecordingComplete(ex); } } else if (res.getStatus() == SipServletResponse.SC_REQUEST_PENDING) { // handle 491 response super.doResponse(res, headers); } else { // pause recording failed pauseRecordingComplete(new SignalException("Re-Invite received error response " + res)); } } else { super.doResponse(res, headers); } } @Override protected synchronized void call(byte[] sdp) throws IOException { if (isNoAnswered()) { if (_invite == null) { createRequest(); } try { // add header _invite.addHeader("Require", "siprec"); _invite.addHeader("MIME-Version", "1.0"); Address contact = _invite.getAddressHeader("Contact"); if (contact != null) { contact.setParameter("+sip.src", ""); } if (sdp != null) { // modify the SDP, 1. modify sendrecv to sendonly 2. add the label // attribute SdpFactory sdpFactory = ((ExecutionContext) getApplicationContext()).getSdpFactory(); SessionDescription sd = sdpFactory.createSessionDescription(new String(sdp, "iso8859-1")); MediaDescription md = ((MediaDescription) sd.getMediaDescriptions(false).get(0)); md.removeAttribute("sendrecv"); md.setAttribute("sendonly", null); // add label md.setAttribute("label", _label); setLocalSDP(sd.toString().getBytes("iso8859-1")); // create multipart content MimeMultipart multiPart = new MimeMultipart(); MimeBodyPart sdpPart = new MimeBodyPart(); sdpPart.setContent(SDPUtils.formulateSDP(this, sd), "application/sdp"); sdpPart.addHeader("Content-Type", "application/sdp"); multiPart.addBodyPart(sdpPart); MimeBodyPart metadataPart = new MimeBodyPart(); metadataPart.setContent(rsMetadata.generateMetadataSnapshot_Draft15().getBytes("iso8859-1"), "application/rs-metadata+xml"); metadataPart.addHeader("Content-Type", "application/rs-metadata+xml"); metadataPart.addHeader("Content-Disposition", "recording-session"); multiPart.addBodyPart(metadataPart); ByteArrayOutputStream out = new ByteArrayOutputStream(); multiPart.writeTo(out); _invite.setContent(out.toByteArray(), multiPart.getContentType()); } } catch (Exception ex) { LOG.error("Exception when creating SIPRecording call.", ex); throw new SignalException(ex); } setSIPCallState(SIPCall.State.INVITING); _invite.send(); } else if (isAnswered()) { reInviteRemote(sdp, null, null); } } protected void doDisconnect(final boolean failed, final CallCompleteEvent.Cause cause, final Exception exception, Map<String, String> headers, SIPCall.State old) { if (LOG.isDebugEnabled()) { LOG.debug(this + " is disconnecting."); } try { if (isNoAnswered(old)) { try { if (_invite != null && (_invite.getSession().getState() == SipSession.State.EARLY || _invite.getSession().getState() == SipSession.State.INITIAL)) { try { SipServletRequest cancelRequest = _invite.createCancel(); SIPHelper.addHeaders(cancelRequest, headers); cancelRequest.send(); } catch (Exception ex) { LOG.warn("Exception when disconnecting failed outbound call:" + ex.getMessage()); _invite.getSession().invalidate(); } } } catch (final Exception t) { LOG.warn("Exception when disconnecting call:" + t.getMessage()); } } else if (isAnswered(old) && _invite.getSession().getState() != SipSession.State.TERMINATED) { try { // update metadata Map<CommunicationSession, Association> communicationSessions = rsMetadata.getCommunicationSessions(); for (CommunicationSession communicationSession : communicationSessions.keySet()) { rsMetadata.disassotiateCommunicationSession(communicationSession); Map<ParticipantMetadata, Association> participants = communicationSession.getParticipants(); for (ParticipantMetadata participant : participants.keySet()) { communicationSession.disassociateParticipant(participant); } Map<MediaStream, Association> mediaStreams = communicationSession.getMediaStreams(); for (MediaStream mediaStream : mediaStreams.keySet()) { communicationSession.disassociateMediaStream(mediaStream); } } SipServletRequest byeReq = _signal.createRequest("BYE"); SIPHelper.addHeaders(byeReq, headers); // add metadata byeReq.setContent(rsMetadata.generateMetadataSnapshot_Draft15().getBytes("iso8859-1"), "application/rs-metadata+xml"); byeReq.addHeader("Content-Disposition", "recording-session"); byeReq.send(); } catch (final Exception t) { LOG.warn("Exception when disconnecting call:" + t.getMessage()); } } } finally { if (_invite != null) { SipApplicationSession appSession = _invite.getApplicationSession(); try { if (appSession.isReadyToInvalidate()) { appSession.invalidate(); if (LOG.isDebugEnabled()) { LOG.debug(appSession.getId() + " invalidated"); } } } catch (IllegalStateException doofus) { try { appSession.invalidate(); if (LOG.isDebugEnabled()) { LOG.debug(appSession.getId() + " invalidated anyway"); } } catch (Exception ex) { LOG.warn("Exception caught while invalidating SipApplicationSession " + appSession.getId(), ex); } } } } terminate(cause, exception, null); } public void pauseRecording() { if (isTerminated()) { LOG.debug("SIPRecording call already terminated." + this); return; } pauseresumingRecording = true; try { SdpFactory sdpFactory = ((ExecutionContext) getApplicationContext()).getSdpFactory(); SessionDescription sd = sdpFactory.createSessionDescription(new String(this.getLocalSDP(), "iso8859-1")); MediaDescription md = ((MediaDescription) sd.getMediaDescriptions(false).get(0)); md.removeAttribute("sendrecv"); md.removeAttribute("sendonly"); md.setAttribute("inactive", null); md.setAttribute("label", _label); setLocalSDP(sd.toString().getBytes("iso8859-1")); reInviteRemote(sd.toString().getBytes("iso8859-1"), null, null); synchronized (condition) { long startTime = System.currentTimeMillis(); while (pauseresumingRecording && System.currentTimeMillis() - startTime <= 40000) { condition.wait(40000); } } if (pauseresumingRecording) { LOG.error("Timeout when pausing SIPRecording " + this); throw new SignalException("Timeout when pausing SIPRecording " + this); } if (asyncException != null) { throw new SignalException(asyncException); } LOG.debug("Paused SIPRecording call:" + this); } catch (Exception ex) { LOG.error("Excetpion when pausing SIPRecording " + this, ex); throw new SignalException(ex); } finally { pauseresumingRecording = false; } } public void resumeRecording() { if (isTerminated()) { LOG.debug("SIPRecording call already terminated." + this); return; } // re-INVITE SRS, sends a new SDP offer and sets the media stream to // sendonly (a=sendonly) pauseresumingRecording = true; try { SdpFactory sdpFactory = ((ExecutionContext) getApplicationContext()).getSdpFactory(); SessionDescription sd = sdpFactory.createSessionDescription(new String(this.getLocalSDP(), "iso8859-1")); MediaDescription md = ((MediaDescription) sd.getMediaDescriptions(false).get(0)); md.removeAttribute("inactive"); md.removeAttribute("sendrecv"); md.setAttribute("sendonly", null); md.setAttribute("label", _label); setLocalSDP(sd.toString().getBytes("iso8859-1")); reInviteRemote(sd.toString().getBytes("iso8859-1"), null, null); synchronized (condition) { long startTime = System.currentTimeMillis(); while (pauseresumingRecording && System.currentTimeMillis() - startTime <= 40000) { condition.wait(40000); } } if (pauseresumingRecording) { LOG.error("Timeout when resuming SIPRecording " + this); throw new SignalException("Timeout when resuming SIPRecording " + this); } if (asyncException != null) { throw new SignalException(asyncException); } LOG.debug("Resumed SIPRecording call:" + this); } catch (Exception ex) { LOG.error("Excetpion when resuming SIPRecording " + this, ex); throw new SignalException(ex); } finally { pauseresumingRecording = false; } } }