package com.rayo.server;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.jms.IllegalStateException;
import javax.media.mscontrol.EventType;
import javax.media.mscontrol.join.Joinable.Direction;
import javax.media.mscontrol.mixer.MediaMixer;
import javax.media.mscontrol.mixer.MixerEvent;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import com.rayo.core.AnsweredEvent;
import com.rayo.core.DtmfCommand;
import com.rayo.core.EndCommand;
import com.rayo.core.EndEvent;
import com.rayo.core.EndEvent.Reason;
import com.rayo.core.HangupCommand;
import com.rayo.core.JoinCommand;
import com.rayo.core.JoinCommand.JoinGroup;
import com.rayo.core.JoinDestinationType;
import com.rayo.core.JoinedEvent;
import com.rayo.core.RingingEvent;
import com.rayo.core.UnjoinCommand;
import com.rayo.core.UnjoinedEvent;
import com.rayo.core.exception.NotAnsweredException;
import com.rayo.core.verb.HoldCommand;
import com.rayo.core.verb.MuteCommand;
import com.rayo.core.verb.Ssml;
import com.rayo.core.verb.UnholdCommand;
import com.rayo.core.verb.UnmuteCommand;
import com.rayo.server.exception.RayoProtocolException;
import com.rayo.server.exception.RayoProtocolException.Condition;
import com.voxeo.exceptions.NotFoundException;
import com.voxeo.logging.Loggerf;
import com.voxeo.moho.Call;
import com.voxeo.moho.Call.State;
import com.voxeo.moho.Joint;
import com.voxeo.moho.Mixer;
import com.voxeo.moho.MixerEndpoint;
import com.voxeo.moho.Participant;
import com.voxeo.moho.Participant.JoinType;
import com.voxeo.moho.common.event.AutowiredEventListener;
import com.voxeo.moho.event.CallCompleteEvent;
import com.voxeo.moho.event.HangupEvent;
import com.voxeo.moho.event.JoinCompleteEvent.Cause;
import com.voxeo.moho.event.UnjoinCompleteEvent;
import com.voxeo.moho.media.output.AudibleResource;
import com.voxeo.moho.media.output.OutputCommand;
import com.voxeo.moho.remotejoin.RemoteParticipant;
import com.voxeo.moho.util.ParticipantIDParser;
public class CallActor <T extends Call> extends AbstractActor<T> {
private static final Loggerf log = Loggerf.getLogger(CallActor.class);
private int JOIN_TIMEOUT = 30000;
private Set<Participant> joinees = new HashSet<Participant>();
private CallStatistics callStatistics;
private CdrManager cdrManager;
private CallRegistry callRegistry;
private MixerManager mixerManager;
private CallManager callManager;
// This is used to synchronize Answered event with media join as Moho may send you
// an answered event before the media is joined
// Also note that no further synchronization is needed as we are within an Actor
private boolean initialJoinReceived = false;
private Map<String, AnsweredEvent> pendingAnswer = new ConcurrentHashMap<String, AnsweredEvent>();
//TODO: MOHO-61
private JoinGroup joinGroup;
public CallActor(T call) {
super(call);
}
// Outgoing Calls
// ================================================================================
@Message
public void onCall(Call call) throws Exception {
try {
log.info("Received call event [%s]", call.getId());
// Now we setup the moho handlers
mohoListeners.add(new AutowiredEventListener(this));
participant.addObserver(new ActorEventListener(this));
String dest = (String)participant.getAttribute(JoinCommand.TO);
if (dest != null) {
JoinDestinationType type = (JoinDestinationType)participant.getAttribute(JoinCommand.TYPE);
javax.media.mscontrol.join.Joinable.Direction direction = participant.getAttribute(JoinCommand.DIRECTION);
JoinType mediaType = participant.getAttribute(JoinCommand.MEDIA_TYPE);
Participant destination = getDestinationParticipant(participant, dest, type);
Boolean force = participant.getAttribute(JoinCommand.FORCE);
joinees.add(destination);
log.info("Executing join operation. Call: [%s]. Join type: [%s]. Direction: [%s]. Participant: [%s].", participant.getId(), mediaType, direction, destination);
participant.join(destination, mediaType, force, direction);
} else {
log.info("Joining call [%s] to media mixer.", participant.getId());
participant.join();
}
callStatistics.outgoingCall();
} catch (Exception e) {
log.error(e.getMessage());
end(Reason.ERROR, e.getMessage());
}
}
@Message
public void onMultipleCalls(Call[] calls) throws Exception {
try {
log.info("Received call event [%s]", calls.toString());
// Now we setup the moho handlers
mohoListeners.add(new AutowiredEventListener(this));
participant.addObserver(new ActorEventListener(this));
log.info("Joining call to multiple participants in Direct mode.", participant.getId());
participant.join(JoinType.DIRECT, true, Direction.DUPLEX, true, calls);
for(int i=0;i<calls.length;i++) {
callStatistics.outgoingCall();
}
} catch (Exception e) {
log.error(e.getMessage());
end(Reason.ERROR, e.getMessage());
}
}
@Override
protected void verbCreated() {
callStatistics.verbCreated();
}
// Call Commands
// ================================================================================
@Message
public void hold(HoldCommand message) {
if (isAnswered(participant)) {
participant.hold();
} else{
throw new NotAnsweredException("Call has not been answered yet");
}
}
@Message
public void unhold(UnholdCommand message) {
if (isAnswered(participant)) {
participant.unhold();
} else{
throw new NotAnsweredException("Call has not been answered yet");
}
}
@Message
public void mute(MuteCommand message) {
if (isAnswered(participant)) {
participant.mute();
} else{
throw new NotAnsweredException("Call has not been answered yet");
}
}
@Message
public void unmute(UnmuteCommand message) {
if (isAnswered(participant)) {
participant.unmute();
} else{
throw new NotAnsweredException("Call has not been answered yet");
}
}
@Message
public void dtmf(DtmfCommand message) {
if(!isAnswered(participant)) {
throw new NotAnsweredException("Call has not been answered yet");
}
Ssml ssml = new Ssml(String.format(
"<audio src=\"dtmf:%s\"/>",message.getTones()));
AudibleResource resource = resolveAudio(ssml);
OutputCommand command = new OutputCommand(resource);
participant.output(command);
/*
if (message.getTones().length() == 1) {
fire(new DtmfEvent(participant.getId(), message.getTones()));
} else {
for (int i = 0; i < message.getTones().length(); i++) {
fire(new DtmfEvent(participant.getId(), String.valueOf(message.getTones().charAt(i))));
}
}
*/
}
@Message
public void join(JoinCommand message) throws Exception {
Participant destination = null;
try {
destination = getDestinationParticipant(participant, message.getTo(), message.getType());
} catch (Exception e) {
if (message.getType() == JoinDestinationType.MIXER) {
log.warn("Trying to join a mixer by raw name [%s] : %s",message.getTo(),e.getMessage());
}
}
if (destination == null) {
if (message.getType() == JoinDestinationType.MIXER) {
// mixer creation
destination = mixerManager.create(getCall().getApplicationContext(), message.getTo());
} else {
throw new NotFoundException("Participant " + message.getTo() + " not found");
}
}
Boolean force = message.getForce() == null ? Boolean.FALSE : message.getForce();
if (message.getType() == JoinDestinationType.MIXER) {
// This synchronized block is required due to the way mixers work in moho. Mixers are
// created and disposed automatically. So before joining and unjoining mixers we need to
// synchronize code to avoid race conditions like would be to disconnect a mixer and at
// the same time having another call trying to join it
synchronized(destination) {
doJoin(destination, message, force);
}
} else {
//#1579867. This may change in the future.
if (destination instanceof Call) {
if (!isAnswered(destination) && !isAnswered(participant)) {
throw new IllegalStateException("None of the calls you are trying to join have been answered.");
}
}
doJoin(destination, message, force);
}
}
private void doJoin(Participant destination, JoinCommand message, boolean force) throws Exception {
Joint joint = participant.join(destination, message.getMedia(), force, message.getDirection());
waitForJoin(joint);
joinees.add(destination);
}
private void waitForJoin(Joint join) throws Exception {
try {
join.get(JOIN_TIMEOUT, TimeUnit.MILLISECONDS);
} catch (TimeoutException te) {
throw new TimeoutException("Timed out while trying to join.");
}
}
@Message
public void unjoin(UnjoinCommand message) throws Exception {
Participant destination = getDestinationParticipant(participant, message.getFrom(), message.getType());
try {
if (destination == null && message.getType() == JoinDestinationType.MIXER) {
MixerEndpoint endpoint = (MixerEndpoint)participant.getApplicationContext()
.createEndpoint(MixerEndpoint.DEFAULT_MIXER_ENDPOINT);
Map<Object, Object> parameters = new HashMap<Object, Object>();
parameters.put(MediaMixer.ENABLED_EVENTS, new EventType[]{MixerEvent.ACTIVE_INPUTS_CHANGED});
destination = endpoint.create(message.getFrom(), parameters);
}
if (destination == null) {
throw new NotFoundException("Participant " + message.getFrom() + " not found");
}
participant.unjoin(destination).get(JOIN_TIMEOUT, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
throw new TimeoutException("Timed out while trying to unjoin.");
}
}
private Participant getDestinationParticipant(
Participant source, String destination, JoinDestinationType type) throws RayoProtocolException {
Participant participant = null;
if (type == JoinDestinationType.CALL) {
CallActor<?> actor = callRegistry.get(destination);
if (actor != null) {
participant = actor.getCall();
}
} else if (type == JoinDestinationType.MIXER) {
participant = mixerManager.getMixer(destination);
} else {
throw new RayoProtocolException(Condition.BAD_REQUEST, "Unknown destination type");
}
if (participant == null && type == JoinDestinationType.CALL) {
// Remote join
log.debug("Detected Remote Destination. Local Source: [%s]. Remote destination: [%s].", source.getId(), destination);
participant = source.getApplicationContext().getParticipant(destination);
log.debug("Remote praticipant: [%s]", participant);
}
return participant;
}
@Message
public void hangup(HangupCommand message) {
// Unjoin app participants before hanging up to get around Moho B2BUA thing
unjoinAll();
participant.hangup(message.getHeaders());
}
@Message
public void end(EndCommand command) {
end(new EndEvent(getParticipantId(), command.getReason(), null));
}
// Moho Events
// ================================================================================
@com.voxeo.moho.State
public void onAnswered(com.voxeo.moho.event.AnsweredEvent<Participant> event) throws Exception {
if(event.getSource().equals(participant)) {
validateMediaOnAnswer(event);
}
}
private void validateMediaOnAnswer(com.voxeo.moho.event.AnsweredEvent<Participant> event) {
AnsweredEvent answeredEvent = new AnsweredEvent(getParticipantId(), event.getHeaders());
if (initialJoinReceived) {
fire(answeredEvent);
} else {
pendingAnswer.put(getParticipantId(), answeredEvent);
}
}
@com.voxeo.moho.State
public void onJoinComplete(com.voxeo.moho.event.JoinCompleteEvent event) {
log.debug("Received Join Complete Event. Is initiator: [%s]", event.isInitiator());
if(event.getSource().equals(participant)) {
Participant peer = event.getParticipant();
log.debug("Join Complete Event source: [%s]. Peer: [%s]", participant, peer);
if (event.getCause() == Cause.JOINED) {
log.debug("Validating media on join");
validateMediaOnJoin(peer);
}
// If the join was successful and either:
// a) initiated via a JoinComand or
// b) initiated by a remote call
if (event.getCause() == Cause.JOINED && (joinees.contains(peer) || !event.isInitiator())) {
if (peer != null) {
String destination = peer.getId();
JoinDestinationType type = null;
if (peer instanceof Mixer) {
type = JoinDestinationType.MIXER;
destination = ((Mixer)peer).getName();
} else if (peer instanceof Call) {
type = JoinDestinationType.CALL;
} else if (peer instanceof RemoteParticipant) {
log.debug("Participant is remote. Trying to guess the type.");
if (ParticipantIDParser.isCall((RemoteParticipant)peer)) {
type = JoinDestinationType.CALL;
} else {
type = JoinDestinationType.MIXER;
destination = ((Mixer)peer).getName();
}
}
joinees.add(peer);
log.debug("Firing Joined event. Participant id: [%s]. Peer id: [%s]. Join type: [%s]", participant.getId(), peer.getId(), type);
fire(new JoinedEvent(participant.getId(), destination, type));
if (type == JoinDestinationType.MIXER) {
// If mixer, we send a participant notification as per Rayo Mixer's spec
fire(new JoinedEvent(destination, participant.getId(), JoinDestinationType.CALL));
}
}
} else {
log.debug("Joined Event not fired. Join cause [%s]. Joinees: [%s]", event.getCause(), joinees);
}
}
}
private void validateMediaOnJoin(Participant peer) {
if (!initialJoinReceived) {
initialJoinReceived = true;
}
if (pendingAnswer.size() > 0) {
validateAnswer(participant);
validateAnswer(peer);
}
}
private void validateAnswer(Participant participant) {
if (participant != null) {
AnsweredEvent answeredEvent = pendingAnswer.get(participant.getId());
if (answeredEvent != null) {
pendingAnswer.remove(participant.getId());
fire(answeredEvent);
}
}
}
@com.voxeo.moho.State
public void onUnjoinEvent(com.voxeo.moho.event.UnjoinCompleteEvent event) {
if(event.getSource().equals(participant)) {
log.debug("Unjoin event received. Participant: [%s], Peer: [%s], Cause: [%s]", participant, event.getParticipant(), event.getCause());
Participant peer = event.getParticipant();
if (peer instanceof Mixer) {
mixerManager.handleCallDisconnect((Mixer)peer, participant);
}
switch(event.getCause()) {
case SUCCESS_UNJOIN:
case DISCONNECT:
if(joinees.contains(peer)) {
fireUnjoinedEvent(event);
joinees.remove(peer);
}
break;
case ERROR:
case FAIL_UNJOIN:
case NOT_JOINED:
log.error(String.format("Call with id %s could not be unjoined from %s [reason=%s]",
participant.getId(), peer, event.getCause()));
}
}
}
private void fireUnjoinedEvent(UnjoinCompleteEvent event) {
if (event.getParticipant() != null) {
JoinDestinationType type = null;
String destination = event.getParticipant().getId();
if (event.getParticipant() instanceof Mixer) {
type = JoinDestinationType.MIXER;
destination = ((Mixer)event.getParticipant()).getName();
} else if (event.getParticipant() instanceof Call) {
type = JoinDestinationType.CALL;
} else if (event.getParticipant() instanceof RemoteParticipant) {
log.debug("Event participant is remote. Trying to guess the type.");
if (ParticipantIDParser.isCall((RemoteParticipant)event.getParticipant())) {
type = JoinDestinationType.CALL;
} else {
type = JoinDestinationType.MIXER;
destination = ((Mixer)event.getParticipant()).getName();
}
}
fire(new UnjoinedEvent(participant.getId(), destination, type));
if (type == JoinDestinationType.MIXER) {
// If mixer, we send a participant notification as per Rayo Mixer's spec
fire(new UnjoinedEvent(destination, participant.getId(), JoinDestinationType.CALL));
}
}
}
@com.voxeo.moho.State
public void onRing(com.voxeo.moho.event.RingEvent event) throws Exception {
if(event.getSource().equals(participant)) {
fire(new RingingEvent(getParticipantId(), event.getHeaders()));
}
}
@com.voxeo.moho.State
public void onCallComplete(CallCompleteEvent event) throws Exception {
if (event.getSource().equals(participant)) {
cdrManager.end(participant);
Reason reason = null;
switch (event.getCause()) {
case BUSY:
callStatistics.callBusy();
reason = Reason.BUSY;
break;
case CANCEL:
case DISCONNECT:
case NEAR_END_DISCONNECT:
callStatistics.callHangedUp();
reason = Reason.HANGUP;
break;
case DECLINE:
case FORBIDDEN:
callStatistics.callRejected();
reason = Reason.REJECT;
break;
case ERROR:
callStatistics.callFailed();
reason = Reason.ERROR;
break;
case TIMEOUT:
callStatistics.callTimedout();
reason = Reason.TIMEOUT;
break;
case REDIRECT:
callStatistics.callRedirected();
reason = Reason.REDIRECT;
break;
default:
callStatistics.callEndedUnknownReason();
throw new UnsupportedOperationException("Reason not handled: " + event.getCause());
}
if (reason == Reason.ERROR) {
end(reason, event.getException(), event.getHeaders());
} else {
end(reason, event.getHeaders());
}
}
}
/*
@com.voxeo.moho.State
public void onDtmf(InputDetectedEvent<Call> event) throws Exception {
if(event.getSource().equals(participant) && event.getInput() != null) {
if (signals != null && signals.contains("dtmf")) {
fire(new SignalEvent(getParticipantId(), "dtmf", event.getInput()));
}
}
}
*/
@com.voxeo.moho.State
public void onHangup(HangupEvent event) throws Exception {
if(event.getSource().equals(participant)) {
unjoinAll();
}
}
boolean isAnswered(Participant participant) {
if (participant instanceof Call) {
Call call = (Call)participant;
if (call.getCallState() == State.CONNECTED) {
return true;
}
}
return false;
}
// Properties
// ================================================================================
public Call getCall() {
return participant;
}
public boolean isOnDirectMedia() {
for (Participant participant: getCall().getParticipants()) {
if (getCall().getJoinType(participant) != JoinType.DIRECT) {
return false;
}
}
return true;
}
public void bridgeMediaIfNecessary() {
if (joinGroup == null) {
if (isOnDirectMedia()) {
log.debug("About to regular media bridging.");
doMediaBridging();
}
} else {
synchronized(joinGroup) {
if (isOnDirectMedia()) {
log.debug("About to do synchronized media bridging.");
doMediaBridging();
}
}
}
}
private void doMediaBridging() {
for (Participant participant: getCall().getParticipants()) {
log.debug("Checking media on participant [%s]", participant.getId());
if (getCall().getJoinType(participant) == JoinType.DIRECT) {
//TODO: MOHO-61
try {
log.info("Unjoining participant [%s] from call", participant.getId());
getCall().unjoin(participant).get();
log.info("Joining participant [%s] to call in BRIDGE_EXCLUSIVE mode.", participant.getId());
getCall().join(participant, JoinType.BRIDGE_EXCLUSIVE, true, Direction.DUPLEX).get();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
log.debug("Done media bridging");
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append("callId", participant.getId()).toString();
}
public CallStatistics getCallStatistics() {
return callStatistics;
}
public void setCallStatistics(CallStatistics callStatistics) {
this.callStatistics = callStatistics;
}
public void setCdrManager(CdrManager cdrManager) {
this.cdrManager = cdrManager;
}
public void setCallRegistry(CallRegistry callRegistry) {
this.callRegistry = callRegistry;
}
public void setMixerManager(MixerManager mixerManager) {
this.mixerManager = mixerManager;
}
public MixerManager getMixerManager() {
return mixerManager;
}
public Set<Participant> getJoinees() {
return new HashSet<Participant>(joinees);
}
public CallManager getCallManager() {
return callManager;
}
public void setCallManager(CallManager callManager) {
this.callManager = callManager;
}
public CallActor<?> getCallActor(String id) {
return callRegistry.get(id);
}
public JoinGroup getJoinGroup() {
return joinGroup;
}
public void setJoinGroup(JoinGroup joinGroup) {
this.joinGroup = joinGroup;
}
}