/** * Copyright 2010 Voxeo Corporation Licensed under the Apache License, Version * 2.0 (the "License"); you may not use this file except in compliance with the * License. You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law * or agreed to in writing, software distributed under the License is * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ package com.voxeo.moho.sip; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.locks.Lock; import javax.media.mscontrol.MsControlException; import javax.media.mscontrol.join.Joinable; import javax.media.mscontrol.join.Joinable.Direction; import javax.media.mscontrol.mediagroup.MediaGroup; import javax.media.mscontrol.mixer.MediaMixer; import javax.media.mscontrol.networkconnection.SdpPortManagerEvent; import javax.servlet.sip.SipServletRequest; import javax.servlet.sip.SipServletResponse; import org.apache.log4j.Logger; import com.voxeo.moho.ApplicationContextImpl; import com.voxeo.moho.BusyException; import com.voxeo.moho.Mixer; import com.voxeo.moho.MixerImpl; import com.voxeo.moho.MixerImpl.ClampDtmfMixerAdapter; import com.voxeo.moho.Participant; import com.voxeo.moho.Participant.JoinType; import com.voxeo.moho.ParticipantContainer; import com.voxeo.moho.RedirectException; import com.voxeo.moho.RejectException; import com.voxeo.moho.SettableJointImpl; import com.voxeo.moho.TimeoutException; import com.voxeo.moho.Unjoint; import com.voxeo.moho.common.event.MohoJoinCompleteEvent; import com.voxeo.moho.common.util.InheritLogContextRunnable; import com.voxeo.moho.event.CallCompleteEvent; import com.voxeo.moho.event.JoinCompleteEvent; import com.voxeo.moho.event.JoinCompleteEvent.Cause; import com.voxeo.moho.remotejoin.RemoteParticipant; import com.voxeo.moho.spi.ExecutionContext; import com.voxeo.moho.util.JoinLockService; public abstract class JoinDelegate { private static final Logger LOG = Logger.getLogger(JoinDelegate.class); protected SettableJointImpl _settableJoint; protected SIPCallImpl _call1; protected SIPCallImpl _call2; protected JoinType _joinType; protected Direction _direction; protected boolean done; protected Cause _cause; protected Exception _exception; protected SIPCallImpl _peer; protected boolean dtmfPassThrough; public void setSettableJoint(SettableJointImpl settableJoint) { _settableJoint = settableJoint; } public void setDtmfPassThrough(boolean dtmfPassThrough) { this.dtmfPassThrough = dtmfPassThrough; } public SettableJointImpl getSettableJoint() { return _settableJoint; } public synchronized void done(final Cause cause, Exception exception) { if (exception != null) { LOG.error("Join complete in error cause:" + cause + " for joinDelegate" + this, exception); } else { LOG.debug("Join complete with cause:" + cause + " for joinDelegate" + this); } if (done) { return; } _cause = cause; _exception = exception; _call1.joinDone(_call2, this); // for remote join Participant p1 = _call1; if (_call1 instanceof RemoteParticipant) { p1 = _call1.getApplicationContext().getParticipant(((RemoteParticipant) _call1).getRemoteParticipantID()); } // for remote join Participant p2 = _call2; if (_call2 != null && _call2 instanceof RemoteParticipant) { p2 = _call2.getApplicationContext().getParticipant(((RemoteParticipant) _call2).getRemoteParticipantID()); } JoinCompleteEvent joinCompleteEvent = new MohoJoinCompleteEvent(p1, p2, cause, exception, _call2 == null ? true : _peer == null ? true : _peer.equals(_call2)); _call1.dispatch(joinCompleteEvent); if (_call2 != null) { _call2.joinDone(_call1, this); JoinCompleteEvent peerJoinCompleteEvent = new MohoJoinCompleteEvent(p2, p1, cause, exception, _peer == null ? false : !_peer.equals(_call2)); _call2.dispatch(peerJoinCompleteEvent); } _settableJoint.done(joinCompleteEvent); done = true; ((ApplicationContextImpl) _call1.getApplicationContext()).getExecutor().execute(new InheritLogContextRunnable() { @Override public void run() { _call1.continueQueuedJoin(); } }); if (_call2 != null) { ((ApplicationContextImpl) _call1.getApplicationContext()).getExecutor().execute(new InheritLogContextRunnable() { @Override public void run() { _call2.continueQueuedJoin(); } }); } } public JoinType getJoinType() { return _joinType; } public SIPCallImpl getInitiator() { return _peer == _call2 ? _call1 : _call2; } public SIPCallImpl getPeer() { return _peer; } public void doJoin() throws Exception { } protected void doInviteResponse(final SipServletResponse res, final SIPCallImpl call, final Map<String, String> headers) throws Exception { throw new UnsupportedOperationException("" + this); } protected void doUpdate(final SipServletRequest req, final SIPCallImpl call, final Map<String, String> headers) throws Exception { throw new UnsupportedOperationException("" + this); } protected void doUpdateResponse(final SipServletResponse resp, final SIPCallImpl call, final Map<String, String> headers) throws Exception { throw new UnsupportedOperationException("" + this); } protected void doPrack(final SipServletRequest req, final SIPCallImpl call, final Map<String, String> headers) throws Exception { req.createResponse(200).send(); } protected void doPrackResponse(final SipServletResponse resp, final SIPCallImpl call, final Map<String, String> headers) throws Exception { // do nothing } protected void doAck(final SipServletRequest req, final SIPCallImpl call) throws Exception { throw new UnsupportedOperationException("" + this); } protected void doSdpEvent(final SdpPortManagerEvent event) { throw new UnsupportedOperationException("" + this); } protected void doDisengage(final SIPCallImpl call, final JoinType type) { if (call.isDirectlyJoined()) { call.unlinkDirectlyPeer(); } else if (call.isBridgeJoined() && type == JoinType.DIRECT) { for (final Participant p : call.getParticipants()) { call.unjoin(p); } call.destroyNetworkConnection(false); } } protected Exception getExceptionByResponse(SipServletResponse res) { if (res == null) { return null; } Exception e = null; if (SIPHelper.isBusy(res)) { e = new BusyException(); } else if (SIPHelper.isRedirect(res)) { e = new RedirectException(res.getHeaders("Contact")); } else if (SIPHelper.isTimeout(res)) { e = new TimeoutException(); } else if (SIPHelper.isDecline(res)) { e = new RejectException(); } else { e = new RejectException(); } return e; } protected CallCompleteEvent.Cause getCallCompleteCauseByResponse(SipServletResponse res) { CallCompleteEvent.Cause cause = null; if (SIPHelper.isBusy(res)) { cause = CallCompleteEvent.Cause.BUSY; } else if (SIPHelper.isRedirect(res)) { cause = CallCompleteEvent.Cause.REDIRECT; } else if (SIPHelper.isTimeout(res)) { cause = CallCompleteEvent.Cause.TIMEOUT; } else if (SIPHelper.isDecline(res)) { cause = CallCompleteEvent.Cause.DECLINE; } else { cause = CallCompleteEvent.Cause.ERROR; } return cause; } protected JoinCompleteEvent.Cause getJoinCompleteCauseByResponse(SipServletResponse res) { JoinCompleteEvent.Cause cause = null; if (SIPHelper.isBusy(res)) { cause = JoinCompleteEvent.Cause.BUSY; } else if (SIPHelper.isRedirect(res)) { cause = JoinCompleteEvent.Cause.REDIRECT; } else if (SIPHelper.isTimeout(res)) { cause = JoinCompleteEvent.Cause.TIMEOUT; } else if (SIPHelper.isDecline(res)) { cause = JoinCompleteEvent.Cause.REJECT; } else if (SIPHelper.isNotAcceptableHere(res)) { cause = JoinCompleteEvent.Cause.NOT_ACCEPTABLE; } else { cause = JoinCompleteEvent.Cause.ERROR; } return cause; } public Cause getCause() { return _cause; } public Exception getException() { return _exception; } public static ExecutionException checkJoinStrategy(final Participant part, final JoinType type, final boolean force) throws Exception { final Participant[] parts = part.getParticipants(); if (parts.length > 0) { if (type == JoinType.DIRECT || type == JoinType.BRIDGE_EXCLUSIVE) { if (force) { // unjoin previous joined Participant Unjoint unjoint = null; if (parts.length > 0) { for (Participant participant : parts) { unjoint = part.unjoin(participant); unjoint.get(); } } } else { // "AlreadyJoined" error return new ExecutionException(JoinDelegate.buildAlreadyJoinedExceptionMessage(part), null); } } else { // BRIDGE_SHARED if (!force) { JoinType checkJoinType = null; if (parts.length > 0) { for (final Participant participant : parts) { checkJoinType = getJoinType(part, participant); if (checkJoinType == JoinType.DIRECT || checkJoinType == JoinType.BRIDGE_EXCLUSIVE) { // "AlreadyJoined" error return new ExecutionException(JoinDelegate.buildAlreadyJoinedExceptionMessage(part), null); } } } } } } return null; } public static ExecutionException checkJoinStrategy(final Participant part, final Participant other, final JoinType type, final boolean force) throws Exception { final Participant[] parts = part.getParticipants(); final Participant[] otherParts = other.getParticipants(); if (parts.length > 0 || otherParts.length > 0) { if (type == JoinType.DIRECT || type == JoinType.BRIDGE_EXCLUSIVE) { if (force) { // unjoin previous joined Participant Unjoint unjoint = null; if (parts.length > 0) { for (Participant participant : parts) { unjoint = part.unjoin(participant); unjoint.get(); } } if (otherParts.length > 0) { for (Participant participant : otherParts) { unjoint = other.unjoin(participant); unjoint.get(); } } } else { // "AlreadyJoined" error return new ExecutionException(parts.length > 0 ? JoinDelegate.buildAlreadyJoinedExceptionMessage(part) : JoinDelegate.buildAlreadyJoinedExceptionMessage(other), null); } } else { // BRIDGE_SHARED if (!force) { JoinType checkJoinType = null; if (parts.length > 0) { for (final Participant participant : parts) { checkJoinType = getJoinType(part, participant); if (checkJoinType == JoinType.DIRECT || checkJoinType == JoinType.BRIDGE_EXCLUSIVE) { // "AlreadyJoined" error return new ExecutionException(JoinDelegate.buildAlreadyJoinedExceptionMessage(part), null); } } } if (otherParts.length > 0) { for (final Participant participant : otherParts) { checkJoinType = getJoinType(other, participant); if (checkJoinType == JoinType.DIRECT || checkJoinType == JoinType.BRIDGE_EXCLUSIVE) { // "AlreadyJoined" error return new ExecutionException(JoinDelegate.buildAlreadyJoinedExceptionMessage(other), null); } } } } } } return null; } protected static JoinType getJoinType(final Participant part, final Participant other) { if (part instanceof SIPCallImpl) { return ((SIPCallImpl) part).getJoinType(other); } if (part instanceof MixerImpl) { return ((MixerImpl) part).getJoinType(other); } return null; } protected static Direction getJoinDirection(final Participant part, final Participant other) { if (part instanceof SIPCallImpl) { return ((SIPCallImpl) part).getDirection(other); } if (part instanceof MixerImpl) { return ((MixerImpl) part).getDirection(other); } return null; } public static void bridgeJoin(final Participant part, final Participant other, final Direction direction) throws MsControlException { LOG.info(part + " joins to " + other + " in " + direction); final Lock lock = JoinLockService.getInstance().get(part, other); lock.lock(); try { // check if part and other has been joined final Direction oldDirection = getJoinDirection(part, other); if (oldDirection != null) { if (oldDirection != direction) { bridgeUnjoin(part, other); } else { LOG.debug(part + "already joined to " + other + " in " + direction + ",ignore the operation"); return; } } final Joinable joinable = getJoinable(part); final Joinable otherJoinable = getJoinable(other); MediaMixer multipleJoiningMixer = null; MediaMixer otherMultipleJoiningMixer = null; final Participant[] parts = part.getParticipants(Direction.RECV); final Participant[] otherParts = other.getParticipants(Direction.RECV); final Joinable[] joinees = joinable.getJoinees(Direction.RECV); final Joinable[] otherJoinees = otherJoinable.getJoinees(Direction.RECV); if (joinees.length == 0 && otherJoinees.length == 0) { joinable.join(direction, otherJoinable); } else { if (joinees.length > 0) { multipleJoiningMixer = getMultipleJoiningMixer(part, true); } if (otherJoinees.length > 0) { otherMultipleJoiningMixer = getMultipleJoiningMixer(other, true); } MediaGroup medGro = getMediaService(part); MediaGroup otherMedGro = getMediaService(other); if (joinees.length > 0) { if (medGro == null) { sharedJoin(part, multipleJoiningMixer, parts[0], other, otherMultipleJoiningMixer, direction); } else { sharedJoin(part, multipleJoiningMixer, medGro, other, otherMultipleJoiningMixer, direction); } } if (otherJoinees.length > 0) { if (otherMedGro == null) { sharedJoin(other, otherMultipleJoiningMixer, otherParts[0], part, multipleJoiningMixer, reserve(direction)); } else { sharedJoin(other, otherMultipleJoiningMixer, otherMedGro, part, multipleJoiningMixer, reserve(direction)); } } } } finally { lock.unlock(); JoinLockService.getInstance().remove(part.getId()); JoinLockService.getInstance().remove(other.getId()); } } public static void bridgeJoin(final Participant part, final MediaGroup medGro) throws MsControlException { LOG.info(part + " joins to " + medGro + " in DUPLEX"); final Lock lock = JoinLockService.getInstance().get(part); lock.lock(); try { final Participant[] parts = part.getParticipants(Direction.RECV); if (parts.length == 0) { getJoinable(part).join(Direction.DUPLEX, medGro); } else { MediaMixer multipleJoiningMixer = getMultipleJoiningMixer(part, true); sharedJoin(part, multipleJoiningMixer, parts[0], medGro); } } finally { lock.unlock(); JoinLockService.getInstance().remove(part.getId()); } } public static void bridgeUnjoin(final Participant part, final Participant other) throws MsControlException { LOG.info(part + " unjoins from " + other); final Lock lock = JoinLockService.getInstance().get(part, other); lock.lock(); try { final Joinable joinable = getJoinable(part); final Joinable otherJoinable = getJoinable(other); if (contains(joinable, otherJoinable, null)) { joinable.unjoin(otherJoinable); } final MediaMixer multipleJoiningMixer = getMultipleJoiningMixer(part, false); final MediaMixer otherMultipleJoiningMixer = getMultipleJoiningMixer(other, false); if (multipleJoiningMixer != null && contains(multipleJoiningMixer, otherJoinable, null)) { multipleJoiningMixer.unjoin(otherJoinable); if (multipleJoiningMixer.getJoinees().length == 1) { destroyMultipleJoiningMixer(part); } } if (otherMultipleJoiningMixer != null && contains(otherMultipleJoiningMixer, joinable, null)) { otherMultipleJoiningMixer.unjoin(joinable); if (otherMultipleJoiningMixer.getJoinees().length == 1) { destroyMultipleJoiningMixer(other); } } } finally { lock.unlock(); JoinLockService.getInstance().remove(part.getId()); JoinLockService.getInstance().remove(other.getId()); } } public static String buildAlreadyJoinedExceptionMessage(final Participant part) { final StringBuffer sbuf = new StringBuffer(); sbuf.append(part + " is already joined."); return sbuf.toString(); } /** * It's used in the following join scenarion, * * <pre> * part.join(peer); * part.join(other); * </pre> * * @param part * A participant has previous join in RECV direction. * @param multipleJoiningMixer * The MediaMixer of "part" used for SHARED join. * @param peer * The Participant connected in RECV direction. * @param other * The new Participant to connect. * @param otherMultipleJoiningMixer * The MediaMixer of "other" used for SHARED join. * @param direction * It indicates direction (DUPLEX, SEND, RECV). * @throws MsControlException */ protected static void sharedJoin(final Participant part, final MediaMixer multipleJoiningMixer, final Participant peer, final Participant other, final MediaMixer otherMultipleJoiningMixer, final Direction direction) throws MsControlException { final Joinable joinable = getJoinable(part); final Joinable otherJoinable = getJoinable(other); if (part instanceof Mixer) { // // we don't need to rejoin its peer Participant in RECV direction, // because Mixer supports "listen to more than one". // _sharedJoin(joinable, multipleJoiningMixer, null, null, null, otherJoinable, otherMultipleJoiningMixer, direction); } else { final Joinable peerJoinable = getJoinable(peer); final MediaMixer peerMultipleJoiningMixer = getMultipleJoiningMixer(peer, false); final Direction peerDirection = part.getDirection(peer); _sharedJoin(joinable, multipleJoiningMixer, peerJoinable, peerDirection, peerMultipleJoiningMixer, otherJoinable, otherMultipleJoiningMixer, direction); } } /** * It's used in the following join scenarion, * * <pre> * part.getMediaService(); * part.join(other); * </pre> * * @param part * A participant has previous join in RECV direction. * @param multipleJoiningMixer * The MediaMixer of "part" used for SHARED join. * @param medGro * The media service of "part". * @param other * The new Participant to connect. * @param otherMultipleJoiningMixer * The MediaMixer of "other" used for SHARED join. * @param direction * It indicates direction (DUPLEX, SEND, RECV). * @throws MsControlException */ protected static void sharedJoin(final Participant part, final MediaMixer multipleJoiningMixer, final MediaGroup medGro, final Participant other, final MediaMixer otherMultipleJoiningMixer, final Direction direction) throws MsControlException { final Joinable joinable = getJoinable(part); final Joinable otherJoinable = getJoinable(other); if (part instanceof Mixer) { // // we don't need to rejoin its MediaService(MediaGroup), // because Mixer supports "listen to more than one". // _sharedJoin(joinable, multipleJoiningMixer, null, null, null, otherJoinable, otherMultipleJoiningMixer, direction); } else { _sharedJoin(joinable, multipleJoiningMixer, medGro, Direction.DUPLEX, null, otherJoinable, otherMultipleJoiningMixer, direction); } } /** * It's used in the following join scenarion, * * <pre> * part.join(peer); * part.getMediaService(); * </pre> * * @param part * A participant has previous join in RECV direction. * @param multipleJoiningMixer * The MediaMixer of "part" used for SHARED join. * @param peer * The Participant connected in RECV direction. * @param medGro * The media service of "part". * @throws MsControlException */ protected static void sharedJoin(final Participant part, final MediaMixer multipleJoiningMixer, final Participant peer, final MediaGroup medGro) throws MsControlException { final Joinable joinable = getJoinable(part); if (part instanceof Mixer) { // // we don't need to rejoin its peer Participant, // because Mixer supports "listen to more than one". // _sharedJoin(joinable, multipleJoiningMixer, null, null, null, medGro, null, Direction.DUPLEX); } else { final Joinable peerJoinable = getJoinable(peer); final Direction peerDirection = part.getDirection(peer); final MediaMixer peerMultipleJoiningMixer = getMultipleJoiningMixer(peer, false); _sharedJoin(joinable, multipleJoiningMixer, peerJoinable, peerDirection, peerMultipleJoiningMixer, medGro, null, Direction.DUPLEX); } } private static void _sharedJoin(final Joinable joinable, final MediaMixer multipleJoiningMixer, final Joinable peerJoinable, final Direction peerDirection, final MediaMixer peerMultipleJoiningMixer, final Joinable otherJoinable, final MediaMixer otherMultipleJoiningMixer, final Direction direction) throws MsControlException { // first rejoin to peer if (peerJoinable != null && !peerJoinable.equals(multipleJoiningMixer)) { if (contains(joinable, peerJoinable, null)) { joinable.unjoin(peerJoinable); } joinable.join(Direction.RECV, multipleJoiningMixer); if (isRecv(peerDirection)) { peerJoinable.join(Direction.SEND, multipleJoiningMixer); } if (isSend(peerDirection)) { if (peerMultipleJoiningMixer == null) { joinable.join(Direction.SEND, peerJoinable); } else { joinable.join(Direction.SEND, peerMultipleJoiningMixer); } } } // then join to other if (joinable instanceof MediaMixer) { // 1, mx.join(mg); // 2, mx1.join(mx2); // 3, mx.join(nc); nc has no previous join if (otherJoinable instanceof MediaGroup || otherJoinable instanceof MediaMixer || otherMultipleJoiningMixer == null) { joinable.join(direction, otherJoinable); } else { // 4, mx.join(nc); nc has previous join if (isRecv(direction)) { otherJoinable.join(Direction.SEND, multipleJoiningMixer); } if (isSend(direction)) { joinable.join(Direction.SEND, otherMultipleJoiningMixer); } } } else { // 5, other cases if (isRecv(direction)) { otherJoinable.join(Direction.SEND, multipleJoiningMixer); } if (isSend(direction)) { if (otherMultipleJoiningMixer == null) { joinable.join(Direction.SEND, otherJoinable); } else { joinable.join(Direction.SEND, otherMultipleJoiningMixer); } } } } public static Joinable getJoinable(final Participant part) { if (part.getMediaObject() instanceof Joinable) { return (Joinable) part.getMediaObject(); } else { // invalid MediaObject type return null; } } public static MediaGroup getMediaService(final Participant part) throws MsControlException { Joinable joinable = getJoinable(part); for (Joinable joinee : joinable.getJoinees()) { if (joinee instanceof MediaGroup) { return (MediaGroup) joinee; } } return null; } protected static MediaMixer getMultipleJoiningMixer(final Participant part, final boolean createIfNotExisted) throws MsControlException { if (part instanceof SIPCallImpl) { final SIPCallImpl call = (SIPCallImpl) part; if (createIfNotExisted && call.getMultipleJoiningMixer() == null) { call.createMultipleJoiningMixer(); } return call.getMultipleJoiningMixer(); } else if (part instanceof MixerImpl) { // MediaMixer supports to listen to more than one resource, so just use // MixerImpl's own MediaMixer final MixerImpl mixer = (MixerImpl) part; return (MediaMixer) mixer.getMediaObject(); } else if (part instanceof ClampDtmfMixerAdapter) { final ClampDtmfMixerAdapter adapter = (ClampDtmfMixerAdapter) part; return (MediaMixer) adapter.getMixer().getMediaObject(); } return null; } protected static void destroyMultipleJoiningMixer(final Participant part) { try { if (part instanceof SIPCallImpl) { final SIPCallImpl call = (SIPCallImpl) part; call.destroyMultipleJoiningMixer(); } } catch (Throwable t) { LOG.warn("Exception when destroying multipleJoiningMixer", t); } } public static boolean contains(final Joinable joinable, final Joinable joinee, final Direction direction) throws MsControlException { if (joinee == null) { return false; } final Joinable[] joinees = direction == null ? joinable.getJoinees() : joinable.getJoinees(direction); for (Joinable j : joinees) { if (j.equals(joinee)) { return true; } } return false; } public static Direction reserve(Direction direction) { if (direction == Direction.RECV) { return Direction.SEND; } else if (direction == Direction.SEND) { return Direction.RECV; } return Direction.DUPLEX; } public static boolean isRecv(Direction direction) { if (direction == Direction.RECV || direction == Direction.DUPLEX) { return true; } return false; } public static boolean isSend(Direction direction) { if (direction == Direction.SEND || direction == Direction.DUPLEX) { return true; } return false; } // used for remote join public void remoteJoinAnswer(byte[] sdp) throws Exception { } protected void disconnectCalls(final List<SIPCallImpl> calls) { ((ExecutionContext) _call1.getApplicationContext()).getExecutor().execute(new InheritLogContextRunnable() { @Override public void run() { for (SIPCallImpl call : calls) { try { call._joinDelegate = null; call.disconnect(false, CallCompleteEvent.Cause.CANCEL, null, null); } catch (Exception ex) { LOG.debug("Exception when disconnecting call " + call); } } calls.clear(); } }); } protected void disconnectCall(final SIPCallImpl call, final boolean fail, final CallCompleteEvent.Cause cause, final Exception ex) { ((ExecutionContext) _call1.getApplicationContext()).getExecutor().execute(new InheritLogContextRunnable() { @Override public void run() { try { call._joinDelegate = null; call.disconnect(fail, cause == null ? (ex != null ? CallCompleteEvent.Cause.ERROR : CallCompleteEvent.Cause.NEAR_END_DISCONNECT) : cause, ex, null); } catch (Exception ex) { LOG.debug("Exception when disconnecting call " + call); } } }); } protected void failCall(final SIPCallImpl call, final Exception ex) { disconnectCall(call, true, null, ex); } }