All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.restcomm.connect.telephony.Conference Maven / Gradle / Ivy

There is a newer version: 8.4.0-227
Show newest version
/*
 * TeleStax, Open Source Cloud Communications
 * Copyright 2011-2014, Telestax Inc and individual contributors
 * by the @authors tag.
 *
 * This program is free software: you can redistribute it and/or modify
 * under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see 
 *
 */
package org.restcomm.connect.telephony;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.mobicents.servlet.restcomm.mscontrol.messages.MediaServerConferenceControllerStateChanged;
import org.restcomm.connect.commons.annotations.concurrency.Immutable;
import org.restcomm.connect.commons.dao.Sid;
import org.restcomm.connect.commons.fsm.Action;
import org.restcomm.connect.commons.fsm.FiniteStateMachine;
import org.restcomm.connect.commons.fsm.State;
import org.restcomm.connect.commons.fsm.Transition;
import org.restcomm.connect.commons.patterns.Observe;
import org.restcomm.connect.commons.patterns.Observing;
import org.restcomm.connect.commons.patterns.StopObserving;
import org.restcomm.connect.dao.CallDetailRecordsDao;
import org.restcomm.connect.dao.ConferenceDetailRecordsDao;
import org.restcomm.connect.dao.DaoManager;
import org.restcomm.connect.dao.entities.ConferenceDetailRecord;
import org.restcomm.connect.mscontrol.api.messages.CreateMediaSession;
import org.restcomm.connect.mscontrol.api.messages.JoinCall;
import org.restcomm.connect.mscontrol.api.messages.JoinComplete;
import org.restcomm.connect.mscontrol.api.messages.Leave;
import org.restcomm.connect.mscontrol.api.messages.Left;
import org.restcomm.connect.mscontrol.api.messages.MediaServerControllerStateChanged.MediaServerControllerState;
import org.restcomm.connect.mscontrol.api.messages.Play;
import org.restcomm.connect.mscontrol.api.messages.StartRecording;
import org.restcomm.connect.mscontrol.api.messages.Stop;
import org.restcomm.connect.mscontrol.api.messages.StopMediaGroup;
import org.restcomm.connect.mscontrol.api.messages.StopRecording;
import org.restcomm.connect.telephony.api.AddParticipant;
import org.restcomm.connect.telephony.api.ConferenceInfo;
import org.restcomm.connect.telephony.api.ConferenceModeratorPresent;
import org.restcomm.connect.telephony.api.ConferenceResponse;
import org.restcomm.connect.telephony.api.ConferenceStateChanged;
import org.restcomm.connect.telephony.api.GetConferenceInfo;
import org.restcomm.connect.telephony.api.RemoveParticipant;
import org.restcomm.connect.telephony.api.StartConference;
import org.restcomm.connect.telephony.api.StopConference;

import akka.actor.ActorRef;
import akka.actor.UntypedActor;
import akka.event.Logging;
import akka.event.LoggingAdapter;
import jain.protocol.ip.mgcp.message.parms.ConnectionMode;

/**
 * @author [email protected] (Thomas Quintana)
 * @author [email protected] (Amit Bhayani)
 * @author [email protected] (Henrique Rosa)
 * @author [email protected] (Maria Farooq)
 */
@Immutable
public final class Conference extends UntypedActor {

    private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);

    // Finite state machine
    private final FiniteStateMachine fsm;
    private final State uninitialized;
    private final State initializing;
    private final State waiting;
    private final State running;
    private final State evicting;
    private final State stopping;
    private final State stopped;
    private final State failed;

    // Runtime stuff
    private final String name;
    private final String accountSid;
    private final String friendlyName;
    private Sid sid;
    private final List calls;
    private final List observers;

    private boolean moderatorPresent = false;

    // Media Session Controller
    private final ActorRef mscontroller;

    private final DaoManager storage;
    private int globalNoOfParticipants;

    private ConferenceStateChanged.State waitingState;

    public Conference(final String name, final ActorRef msController, final DaoManager storage) {
        super();
        final ActorRef source = self();

        // Finite states
        this.uninitialized = new State("uninitialized", null, null);
        this.initializing = new State("initializing", new Initializing(source));
        this.waiting = new State("waiting", new Waiting(source));
        this.running = new State("running", new Running(source));
        this.evicting = new State("evicting", new Evicting(source));
        this.stopping = new State("stopping", new Stopping(source));
        this.stopped = new State("stopped", new Stopped(source));
        this.failed = new State("failed", new Failed(source));

        // State transitions
        final Set transitions = new HashSet();
        transitions.add(new Transition(uninitialized, initializing));
        transitions.add(new Transition(initializing, waiting));
        transitions.add(new Transition(initializing, stopping));
        transitions.add(new Transition(initializing, failed));
        transitions.add(new Transition(waiting, running));
        transitions.add(new Transition(waiting, evicting));
        transitions.add(new Transition(waiting, stopping));
        transitions.add(new Transition(running, evicting));
        transitions.add(new Transition(running, stopping));
        transitions.add(new Transition(evicting, stopping));
        transitions.add(new Transition(stopping, stopped));
        transitions.add(new Transition(stopping, failed));

        // Finite state machine
        this.fsm = new FiniteStateMachine(uninitialized, transitions);

        // Runtime stuff
        this.name = name;
        final String[] cnfNameAndAccount = name.split(":");
        accountSid = cnfNameAndAccount[0];
        friendlyName = cnfNameAndAccount[1];

        this.storage = storage;

        //generate it later at MRB level, by watching if same conference is running on another RC instance.
        //this.sid = Sid.generate(Sid.Type.CONFERENCE);
        this.mscontroller = msController;
        this.calls = new ArrayList();
        this.observers = new ArrayList();
    }

    private boolean is(State state) {
        return this.fsm.state().equals(state);
    }

    private boolean isRunning() {
        return is(waiting) || is(running);
    }

    private void broadcast(final Object message) {
        if (!this.observers.isEmpty()) {
            final ActorRef self = self();
            for (ActorRef observer : observers) {
                if (!observer.isTerminated()) {
                    observer.tell(message, self);
                } else {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Conference broadcase, Observer is terminated: "+observer.path());
                    }
                }
            }
        }
    }

    @Override
    public void onReceive(Object message) throws Exception {
        final Class klass = message.getClass();
        final ActorRef sender = sender();
        ActorRef self = self();
        final State state = fsm.state();

        if (logger.isInfoEnabled()) {
            logger.info(" ********** Conference " + self().path() + " Current State: " + state.toString());
            logger.info(" ********** Conference " + self().path() + " Processing Message: " + klass.getName());
        }

        if (Observe.class.equals(klass)) {
            onObserve((Observe) message, self, sender);
        } else if (StopObserving.class.equals(klass)) {
            onStopObserving((StopObserving) message, self, sender);
        } else if (GetConferenceInfo.class.equals(klass)) {
            onGetConferenceInfo(self, sender);
        } else if (StartConference.class.equals(klass)) {
            onStartConference((StartConference) message, self, sender);
        } else if (StopConference.class.equals(klass)) {
            onStopConference((StopConference) message, self, sender);
        } else if (ConferenceModeratorPresent.class.equals(klass)) {
            onConferenceModeratorPresent((ConferenceModeratorPresent) message, self, sender);
        } else if (AddParticipant.class.equals(klass)) {
            onAddParticipant((AddParticipant) message, self, sender);
        } else if (RemoveParticipant.class.equals(klass)) {
            onRemoveParticipant((RemoveParticipant) message, self, sender);
        } else if (Left.class.equals(klass)) {
            onLeft((Left) message, self, sender);
        } else if (JoinComplete.class.equals(klass)) {
            onJoinComplete((JoinComplete) message, self, sender);
        } else if (MediaServerConferenceControllerStateChanged.class.equals(klass)) {
            onMediaServerControllerStateChanged((MediaServerConferenceControllerStateChanged) message, self, sender);
        } else if (Play.class.equals(klass)) {
            onPlay((Play) message, self, sender);
        } else if (StartRecording.class.equals(klass)) {
            onStartRecording((StartRecording) message, self, sender);
        } else if (StopRecording.class.equals(klass)) {
            onStopRecording((StopRecording) message, self, sender);
        }
    }

    /*
     * ACTIONS
     */
    private abstract class AbstractAction implements Action {

        protected final ActorRef source;

        public AbstractAction(final ActorRef source) {
            super();
            this.source = source;
        }
    }

    private class Initializing extends AbstractAction {

        public Initializing(ActorRef source) {
            super(source);
        }

        @Override
        public void execute(Object message) throws Exception {
            StartConference startConference = (StartConference) message;
            // Start observing state changes in the MSController
            final Observe observe = new Observe(super.source);
            mscontroller.tell(observe, super.source);

            ConferenceInfo information = createConferenceInfo();
            // Initialize the MS Controller
            final CreateMediaSession createMediaSession = new CreateMediaSession(startConference.callSid(), information.name());
            mscontroller.tell(createMediaSession, super.source);
        }

    }

    private final class Waiting extends AbstractAction {
        public Waiting(final ActorRef source) {
            super(source);
        }

        @Override
        public void execute(final Object message) throws Exception {
            //As MRB have generated Sid for this conference and saved in db.
            MediaServerConferenceControllerStateChanged mediaServerConferenceControllerStateChanged = (MediaServerConferenceControllerStateChanged) message;
            sid = mediaServerConferenceControllerStateChanged.conferenceSid();
            String stateStr= mediaServerConferenceControllerStateChanged.conferenceState();

            //this is to cover the scenario where initial state is not moderatorAbsent and maybe moderator is present on another node.
            waitingState = ConferenceStateChanged.translateState(stateStr, ConferenceStateChanged.State.RUNNING_MODERATOR_ABSENT);
            moderatorPresent = mediaServerConferenceControllerStateChanged.moderatorPresent();
            if(logger.isInfoEnabled()) {
                logger.info("################################## Conference " + name + " has sid: "+sid +" stateStr: "+stateStr+" initial state: "+waitingState);
            }
            broadcast(new ConferenceStateChanged(name, waitingState));
        }
    }

    private final class Running extends AbstractAction {
        public Running(final ActorRef source) {
            super(source);
        }

        @Override
        public void execute(final Object message) throws Exception {
            // Stop the background music if present
            mscontroller.tell(new StopMediaGroup(), super.source);
            updateConferenceStatus(ConferenceStateChanged.State.RUNNING_MODERATOR_PRESENT);
            // Notify the observers
            broadcast(new ConferenceStateChanged(name, ConferenceStateChanged.State.RUNNING_MODERATOR_PRESENT));
        }
    }

    private class Evicting extends AbstractAction {

        public Evicting(ActorRef source) {
            super(source);
        }

        @Override
        public void execute(Object message) throws Exception {
            // Tell every participant to leave the conference room
            // NOTE: calls list will only be update in the onLeft() event!
            for (final ActorRef call : calls) {
                final Leave leave = new Leave();
                call.tell(leave, super.source);
            }
        }

    }

    private class Stopping extends AbstractAction {

        public Stopping(ActorRef source) {
            super(source);
        }

        @Override
        public void execute(Object message) throws Exception {
            // Ask the MS Controller to stop
            // This will stop any current media operations and clean media resources
            mscontroller.tell(new Stop(), super.source);
        }
    }

    private abstract class FinalizingAction extends AbstractAction {

        protected final ConferenceStateChanged.State finalState;

        public FinalizingAction(ActorRef source, ConferenceStateChanged.State state) {
            super(source);
            finalState = state;
        }

        @Override
        public void execute(Object message) throws Exception {
            // Notify the observers.
            broadcast(new ConferenceStateChanged(name, this.finalState));
            observers.clear();
        }

    }

    private final class Stopped extends FinalizingAction {

        public Stopped(final ActorRef source) {
            super(source, ConferenceStateChanged.State.COMPLETED);
        }

    }

    private final class Failed extends FinalizingAction {

        public Failed(final ActorRef source) {
            super(source, ConferenceStateChanged.State.FAILED);
        }

    }

    /*
     * EVENTS
     */
    private void onObserve(Observe message, ActorRef self, ActorRef sender) {
        final ActorRef observer = message.observer();
        if (observer != null) {
            this.observers.add(observer);
            observer.tell(new Observing(self), self);
        }
    }

    private void onStopObserving(StopObserving message, ActorRef self, ActorRef sender) {
        final ActorRef observer = message.observer();
        if (observer != null) {
            observers.remove(observer);
        }
    }

    private void onGetConferenceInfo(ActorRef self, ActorRef sender) throws Exception {
        sender.tell(new ConferenceResponse(createConferenceInfo()), self);
    }

    private ConferenceInfo createConferenceInfo() throws Exception{
        ConferenceInfo information = null;
        int globalNoOfParticipants = getGlobalNoOfParticipants();
        if (is(waiting)) {
            information = new ConferenceInfo(sid, calls, waitingState, name, moderatorPresent, globalNoOfParticipants);
        } else if (is(running)) {
            information = new ConferenceInfo(sid, calls, ConferenceStateChanged.State.RUNNING_MODERATOR_PRESENT, name, moderatorPresent, globalNoOfParticipants);
        } else if (is(stopped)) {
            information = new ConferenceInfo(sid, calls, ConferenceStateChanged.State.COMPLETED, name, moderatorPresent, globalNoOfParticipants);
        } else {
            information = new ConferenceInfo(sid, calls, null, name, moderatorPresent, globalNoOfParticipants);
        }
        return information;
    }

    private void onStartConference(StartConference message, ActorRef self, ActorRef sender) throws Exception {
        if (is(uninitialized)) {
            this.fsm.transition(message, initializing);
        }
    }

    private void onStopConference(StopConference message, ActorRef self, ActorRef sender) throws Exception {
        if (is(initializing)) {
            this.fsm.transition(message, stopped);
        } else if (is(waiting) || is(running)) {
            this.fsm.transition(message, evicting);
        }
    }

    private void onConferenceModeratorPresent(ConferenceModeratorPresent message, ActorRef self, ActorRef sender)
            throws Exception {
        if (is(waiting)) {
            this.fsm.transition(message, running);
        }
    }

    private void onAddParticipant(AddParticipant message, ActorRef self, ActorRef sender) {
        if (isRunning()) {
            final JoinCall joinCall = new JoinCall(message.call(), ConnectionMode.Confrnce, this.sid);
            this.mscontroller.tell(joinCall, self);
        }
    }

    private void onRemoveParticipant(RemoveParticipant message, ActorRef self, ActorRef sender) throws Exception {
        if (isRunning()) {
            if (logger.isInfoEnabled()) {
                logger.info("Received RemoveParticipants for Call: "+message.call().path());
            }
            // Kindly ask participant to leave
            final ActorRef call = message.call();
            final Leave leave = new Leave();
            call.tell(leave, self);
        } else {
            if (logger.isDebugEnabled()) {
                logger.debug("Received RemoveParticipants for Call: "+message.call().path()+" but the state is: "+fsm.state().toString());
            }
        }
    }

    private void onLeft(Left message, ActorRef self, ActorRef sender) throws Exception {
        if (is(running) || is(waiting) || is(evicting)) {
            // Participant successfully left the conference.
            boolean removed = calls.remove(sender);
            int participantsNr = calls.size();
            if(logger.isInfoEnabled()) {
                logger.info("################################## Conference " + name + " has " + participantsNr + " participants");
            }
            ConferenceResponse conferenceResponse = new ConferenceResponse(message);
            broadcast(conferenceResponse);
            if (logger.isDebugEnabled()) {
                logger.debug("Call left conference room and notification sent to observers.");
            }

            // Stop the conference when ALL participants have been evicted
            if (removed && calls.isEmpty()) {
                fsm.transition(message, stopping);
            }
        }
    }

    private void onMediaServerControllerStateChanged(MediaServerConferenceControllerStateChanged message, ActorRef self, ActorRef sender)
            throws Exception {
        MediaServerControllerState state = message.getState();
        switch (state) {
            case ACTIVE:
                if (is(initializing)) {
                    this.fsm.transition(message, waiting);
                }
                break;
            case INACTIVE:
                if (is(stopping)) {
                    this.fsm.transition(message, stopped);
                }
                break;
            case FAILED:
                if (is(initializing)) {
                    this.fsm.transition(message, failed);
                }
                break;
            default:
                // ignore unknown state
                break;
        }
    }

    private void onJoinComplete(JoinComplete message, ActorRef self, ActorRef sender) throws Exception {
        this.mscontroller.tell(message, sender);
        this.calls.add(sender);
        if (logger.isInfoEnabled()) {
            logger.info("Conference name: "+name+", path: "+self().path()+", received JoinComplete from Call: "+sender.path()+", number of participants currently: "+calls.size()+", will send conference info to observers");
        }
        if (observers != null && observers.size() > 0) {
            Iterator iter = observers.iterator();
            ConferenceInfo ci = createConferenceInfo();
            sender.tell(new ConferenceResponse(ci), self());
            while (iter.hasNext()) {
                ActorRef observer = iter.next();
                //First send conferenceInfo
                observer.tell(new ConferenceResponse(ci), self());
                //Next send the JoinComplete message
                observer.tell(message, self());
            }
        }
    }

    private void onPlay(Play message, ActorRef self, ActorRef sender) {
        if (isRunning()) {
            moderatorPresent = message.isConfModeratorPresent();
            if (logger.isInfoEnabled()) {
                logger.info("Received Play message for conference: "+this.name+" , number of local participants: "+this.calls.size()+ " globalNoOfParticipants: "+globalNoOfParticipants+", isRunning: true, isModeratorPresent: "+this.moderatorPresent + " iterations: "+message.iterations());
            }
            // Forward message to media server controller
            this.mscontroller.tell(message, sender);
        } else {
            if (logger.isInfoEnabled()) {
                logger.info("Play will not be processed for conference: "+this.name+" , number of local participants: "+this.calls.size()+ " globalNoOfParticipants: "+globalNoOfParticipants+" , isRunning: false, isModeratorPresent: "+this.moderatorPresent+ " iterations: "+message.iterations());
            }
        }
    }

    private void onStartRecording(StartRecording message, ActorRef self, ActorRef sender) {
        if (isRunning()) {
            // Forward message to media server controller
            this.mscontroller.tell(message, sender);
        }
    }

    private void onStopRecording(StopRecording message, ActorRef self, ActorRef sender) {
        if (isRunning()) {
            // Forward message to media server controller
            this.mscontroller.tell(message, sender);
        }
    }

    /**
     * get global total no of participants from db
     * @throws Exception
     */
    private int getGlobalNoOfParticipants() throws Exception{
        if(sid == null){
            globalNoOfParticipants = calls.size();
        }else{
            CallDetailRecordsDao dao = storage.getCallDetailRecordsDao();
            globalNoOfParticipants = dao.getTotalRunningCallDetailRecordsByConferenceSid(sid);
        }
        if(logger.isInfoEnabled())
            logger.info("sid: "+sid+"globalNoOfParticipants: "+globalNoOfParticipants);
        return globalNoOfParticipants;
    }

    private void updateConferenceStatus(ConferenceStateChanged.State state){
        if(sid != null){
            final ConferenceDetailRecordsDao dao = storage.getConferenceDetailRecordsDao();
            ConferenceDetailRecord cdr = dao.getConferenceDetailRecord(sid);
            cdr = cdr.setStatus(state.name());
            dao.updateConferenceDetailRecordStatus(cdr);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy