/*
* Jicofo, the Jitsi Conference Focus.
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jitsi.jicofo;
import net.java.sip.communicator.service.shutdown.*;
import net.java.sip.communicator.util.*;
import net.java.sip.communicator.util.Logger;
import org.jitsi.jicofo.log.*;
import org.jitsi.protocol.*;
import org.jitsi.service.configuration.*;
import org.jitsi.util.*;
import org.jitsi.videobridge.log.*;
import org.jivesoftware.smack.provider.*;
import java.util.*;
/**
* Manages {@link JitsiMeetConference} on some server. Takes care of creating
* and expiring conference focus instances.
*
* @author Pawel Domas
*/
public class FocusManager
implements JitsiMeetConference.ConferenceListener
{
/**
* The logger used by this instance.
*/
private final static Logger logger = Logger.getLogger(FocusManager.class);
/**
* Name of configuration property for focus idle timeout.
*/
public static final String IDLE_TIMEOUT_PROP_NAME
= "org.jitsi.focus.IDLE_TIMEOUT";
/**
* Default amount of time for which the focus is being kept alive in idle
* mode(no peers in the room).
*/
public static final long DEFAULT_IDLE_TIMEOUT = 15000;
/**
* The name of configuration property that specifies server hostname to
* which the focus user will connect to.
*/
public static final String HOSTNAME_PNAME = "org.jitsi.jicofo.HOSTNAME";
/**
* The name of configuration property that specifies XMPP domain that hosts
* the conference and will be used in components auto-discovery. This is the
* domain on which the jitsi-videobridge runs.
*/
public static final String XMPP_DOMAIN_PNAME
= "org.jitsi.jicofo.XMPP_DOMAIN";
/**
* The name of configuration property that specifies XMPP domain of
* the focus user.
*/
public static final String FOCUS_USER_DOMAIN_PNAME
= "org.jitsi.jicofo.FOCUS_USER_DOMAIN";
/**
* The name of configuration property that specifies the user name used by
* the focus to login to XMPP server.
*/
public static final String FOCUS_USER_NAME_PNAME
= "org.jitsi.jicofo.FOCUS_USER_NAME";
/**
* The name of configuration property that specifies login password of the
* focus user. If not provided then anonymous login method is used.
*/
public static final String FOCUS_USER_PASSWORD_PNAME
= "org.jitsi.jicofo.FOCUS_USER_PASSWORD";
/**
* The address of XMPP server to which the focus user will connect to.
*/
private String hostName;
/**
* The XMPP domain used by the focus user to register to.
*/
private String focusUserDomain;
/**
* Optional focus user password(if null then will login anonymously).
*/
private String focusUserPassword;
/**
* The thread that expires {@link JitsiMeetConference}s.
*/
private FocusExpireThread expireThread = new FocusExpireThread();
/**
* Jitsi Meet conferences mapped by MUC room names.
*/
private Map<String, JitsiMeetConference> conferences
= new HashMap<String, JitsiMeetConference>();
/**
* <tt>JitsiMeetServices</tt> instance that recognizes currently available
* conferencing services like Jitsi videobridge or SIP gateway.
*/
private JitsiMeetServices jitsiMeetServices;
/**
* Indicates if graceful shutdown mode has been enabled and
* no new conference request will be accepted.
*/
private boolean shutdownInProgress;
/**
* Starts this manager for given <tt>hostName</tt>.
*/
public void start()
{
expireThread.start();
ConfigurationService config = FocusBundleActivator.getConfigService();
hostName = config.getString(HOSTNAME_PNAME);
String xmppDomain = config.getString(XMPP_DOMAIN_PNAME);
focusUserDomain = config.getString(FOCUS_USER_DOMAIN_PNAME);
String focusUserName = config.getString(FOCUS_USER_NAME_PNAME);
focusUserPassword = config.getString(FOCUS_USER_PASSWORD_PNAME);
jitsiMeetServices = new JitsiMeetServices();
jitsiMeetServices.start(hostName, xmppDomain,
focusUserDomain, focusUserName, focusUserPassword);
ProviderManager
.getInstance()
.addExtensionProvider(LogPacketExtension.LOG_ELEM_NAME,
LogPacketExtension.NAMESPACE,
new LogExtensionProvider());
FocusBundleActivator
.bundleContext.registerService(
JitsiMeetServices.class, jitsiMeetServices, null);
}
/**
* Stops this instance.
*/
public void stop()
{
expireThread.stop();
jitsiMeetServices.stop();
}
/**
* Allocates new focus for given MUC room.
* @param room the name of MUC room for which new conference has to be
* allocated.
* @param properties configuration properties map included in the request.
* @return <tt>true</tt> if conference focus is in the room and ready to
* handle session participants.
*/
public synchronized boolean conferenceRequest(
String room, Map<String, String> properties)
{
if (StringUtils.isNullOrEmpty(room))
return false;
if (shutdownInProgress && !conferences.containsKey(room))
return false;
if (!conferences.containsKey(room))
{
createConference(room, properties);
}
JitsiMeetConference conference = conferences.get(room);
return conference.isInTheRoom();
}
/**
* Makes sure that conference is allocated for given <tt>room</tt>.
* @param room name of the MUC room of Jitsi Meet conference.
* @param properties configuration properties, see {@link JitsiMeetConfig}
* for the list of valid properties.
*/
private void createConference(String room, Map<String, String> properties)
{
JitsiMeetConfig config = new JitsiMeetConfig(properties);
JitsiMeetConference conference
= new JitsiMeetConference(
room, hostName, focusUserDomain, focusUserPassword,
this, config);
try
{
conferences.put(room, conference);
StringBuilder options = new StringBuilder();
for (Map.Entry<String, String> option : properties.entrySet())
{
options.append("\n ")
.append(option.getKey())
.append(": ")
.append(option.getValue());
}
logger.info("Created new focus for " + room + "@" + focusUserDomain
+ " conferences count: " + conferences.size()
+ " options:" + options.toString());
LoggingService loggingService
= FocusBundleActivator.getLoggingService();
if (loggingService != null)
{
loggingService.logEvent(
LogEventFactory.focusCreated(room + "@" + focusUserDomain));
}
conference.start();
}
catch (Exception e)
{
logger.error("Failed to start conference for room: " + room, e);
}
}
/**
* {@inheritDoc}
*/
@Override
public synchronized void conferenceEnded(JitsiMeetConference conference)
{
String roomName = conference.getRoomName();
conferences.remove(roomName);
logger.info(
"Disposed conference for room: " + roomName
+ " conference count: " + conferences.size());
maybeDoShutdown();
}
/**
* Returns {@link JitsiMeetConference} for given MUC <tt>roomName</tt>
* or <tt>null</tt> if no conference has been allocated yet.
*
* @param roomName the name of MUC room for which we want get the
* {@link JitsiMeetConference} instance.
*/
public JitsiMeetConference getConference(String roomName)
{
return conferences.get(roomName);
}
/**
* Enables shutdown mode which means that no new focus instances will
* be allocated. After conference count drops to zero the process will exit.
*/
public void enableGracefulShutdownMode()
{
if (!this.shutdownInProgress)
{
logger.info("Focus entered graceful shutdown mode");
}
this.shutdownInProgress = true;
maybeDoShutdown();
}
private void maybeDoShutdown()
{
if (shutdownInProgress && conferences.size() == 0)
{
logger.info("Focus is shutting down NOW");
ShutdownService shutdownService
= ServiceUtils.getService(
FocusBundleActivator.bundleContext,
ShutdownService.class);
shutdownService.beginShutdown();
}
}
/**
* Returns the number of currently allocated focus instances.
*/
public int getConferenceCount()
{
return conferences.size();
}
/**
* Returns <tt>true</tt> if graceful shutdown mode has been enabled and
* the process is going to be finished once conference count drops to zero.
*/
public boolean isShutdownInProgress()
{
return shutdownInProgress;
}
/**
* Method should be called by authentication component when user identified
* by given <tt>realJid</tt> has been authenticated for given
* <tt>identity</tt>.
*
* @param roomName the name of the conference room for which authentication
* has occurred.
* @param realJid the real JID of authenticated user
* (not MUC jid which can be faked).
* @param identity confirmed identity of the user.
*/
public void userAuthenticated(String roomName,
String realJid, String identity)
{
JitsiMeetConference conference = conferences.get(roomName);
if (conference == null)
{
logger.error(
"Auth request - no active conference for room: " + roomName);
return;
}
conference.userAuthenticated(realJid, identity);
}
/**
* Class takes care of stopping {@link JitsiMeetConference} if there is no
* active session for too long.
*/
class FocusExpireThread
{
private static final long POLL_INTERVAL = 5000;
private final long timeout;
private Thread timeoutThread;
private final Object sleepLock = new Object();
private boolean enabled;
public FocusExpireThread()
{
timeout = FocusBundleActivator.getConfigService()
.getLong(IDLE_TIMEOUT_PROP_NAME, DEFAULT_IDLE_TIMEOUT);
}
void start()
{
if (timeoutThread != null)
{
throw new IllegalStateException();
}
timeoutThread = new Thread(new Runnable()
{
@Override
public void run()
{
expireLoop();
}
}, "FocusExpireThread");
enabled = true;
timeoutThread.start();
}
void stop()
{
if (timeoutThread == null)
{
return;
}
enabled = false;
synchronized (sleepLock)
{
sleepLock.notifyAll();
}
try
{
if (Thread.currentThread() != timeoutThread)
{
timeoutThread.join();
}
timeoutThread = null;
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}
}
private void expireLoop()
{
while (enabled)
{
// Sleep
try
{
synchronized (sleepLock)
{
sleepLock.wait(POLL_INTERVAL);
}
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
}
if (!enabled)
break;
// Loop over conferences
for (JitsiMeetConference conference
: new ArrayList<JitsiMeetConference>(conferences.values()))
{
long idleStamp = conference.getIdleTimestamp();
// Is active ?
if (idleStamp == -1)
{
continue;
}
if (System.currentTimeMillis() - idleStamp > timeout)
{
logger.info(
"Focus idle timeout for "
+ conference.getRoomName());
conference.stop();
}
}
}
}
}
}