You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openmeetings.apache.org by so...@apache.org on 2020/10/22 05:41:41 UTC

[openmeetings] branch master updated: [OPENMEETINGS-2239, OPENMEETINGS-677] sip transport with user count is added to the room

This is an automated email from the ASF dual-hosted git repository.

solomax pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openmeetings.git


The following commit(s) were added to refs/heads/master by this push:
     new 401e498  [OPENMEETINGS-2239, OPENMEETINGS-677] sip transport with user count is added to the room
401e498 is described below

commit 401e4981c184764d21036204a75ef5dd3098c36e
Author: Maxim Solodovnik <so...@gmail.com>
AuthorDate: Thu Oct 22 12:35:47 2020 +0700

    [OPENMEETINGS-2239, OPENMEETINGS-677] sip transport with user count is added to the room
---
 openmeetings-core/pom.xml                          |   8 +
 .../openmeetings/core/remote/KurentoHandler.java   |  85 ++---
 .../openmeetings/core/remote/StreamProcessor.java  |  15 +-
 .../apache/openmeetings/core/sip/SipManager.java   | 108 +++++--
 .../core/util/ChatWebSocketHelper.java             |   5 +-
 .../openmeetings/core/util/WebSocketHelper.java    |  33 +-
 openmeetings-db/pom.xml                            |   9 -
 .../db/dao/calendar/AppointmentDao.java            |   2 +-
 .../apache/openmeetings/db/dao/room/RoomDao.java   |   7 +-
 .../apache/openmeetings/db/dao/room/SipConfig.java | 123 -------
 .../openmeetings/db/entity/basic/Client.java       |   5 +
 .../openmeetings/db/manager/IClientManager.java    |   6 +-
 .../{dao/room => manager}/IInvitationManager.java  |   2 +-
 .../{IClientManager.java => ISipManager.java}      |  20 +-
 .../openmeetings/db/util/ApplicationHelper.java    |   8 +-
 .../openmeetings/db/util/ws/RoomMessage.java       |   2 +-
 .../installation/ImportInitvalues.java             |   4 +-
 .../src/site/markdown/AsteriskIntegration.md       |   9 +-
 .../src/site/markdown/InstallMediaServer.md        |   2 +-
 openmeetings-server/src/site/site.xml              |   2 +-
 .../src/site/xdoc/PrivacyStatement.xml             |  12 +-
 .../src/site/xdoc/voip-sip-integration.xml         | 360 ---------------------
 .../service/notifier/MailNotifier.java             |   2 +-
 .../service/room/InvitationManager.java            |   2 +-
 .../{quartz => }/scheduler/AbstractJob.java        |   2 +-
 .../service/{quartz => }/scheduler/AtomReader.java |   2 +-
 .../service/{quartz => }/scheduler/CleanupJob.java |  25 +-
 .../{quartz => }/scheduler/ReminderJob.java        |   2 +-
 .../web/admin/connection/ConnectionsPanel.java     |  12 +-
 .../openmeetings/web/admin/rooms/RoomForm.java     |   3 +-
 .../apache/openmeetings/web/app/Application.java   |  10 +
 .../apache/openmeetings/web/app/ClientManager.java |  86 ++---
 .../apache/openmeetings/web/app/TimerService.java  |  61 +++-
 .../apache/openmeetings/web/app/UserManager.java   |   6 +-
 .../apache/openmeetings/web/room/RoomPanel.java    |  51 ++-
 .../web/room/menu/SipDialerDialog.java             |   4 +-
 .../web/room/wb/WbWebSocketHelper.java             |   3 +-
 .../openmeetings/web/user/MessageDialog.java       |   2 +-
 .../openmeetings/web/user/rooms/RoomListPanel.java |   4 +-
 .../openmeetings/web/user/rooms/RoomsPanel.java    |   3 +-
 .../webapp/WEB-INF/classes/applicationContext.xml  |  51 +--
 .../webapp/WEB-INF/classes/openmeetings.properties |  65 ++++
 .../openmeetings/service/quartz/TestJob.java       |   4 +-
 .../openmeetings/webservice/RoomWebService.java    |  12 +-
 pom.xml                                            |  37 ++-
 45 files changed, 433 insertions(+), 843 deletions(-)

diff --git a/openmeetings-core/pom.xml b/openmeetings-core/pom.xml
index dd82ded..936ee4c 100644
--- a/openmeetings-core/pom.xml
+++ b/openmeetings-core/pom.xml
@@ -105,6 +105,14 @@
 			<groupId>org.kurento</groupId>
 			<artifactId>kurento-client</artifactId>
 		</dependency>
+		<dependency>
+			<groupId>org.asteriskjava</groupId>
+			<artifactId>asterisk-java</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>javax.sip</groupId>
+			<artifactId>jain-sip-ri</artifactId>
+		</dependency>
 		<!-- Test dependencies -->
 		<dependency>
 			<groupId>org.junit.jupiter</groupId>
diff --git a/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KurentoHandler.java b/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KurentoHandler.java
index 0aa2934..5f1214e 100644
--- a/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KurentoHandler.java
+++ b/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KurentoHandler.java
@@ -71,10 +71,13 @@ import org.kurento.jsonrpc.client.JsonRpcClientNettyWebSocket;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
 
 import com.github.openjson.JSONArray;
 import com.github.openjson.JSONObject;
 
+@Component
 public class KurentoHandler {
 	private static final Logger log = LoggerFactory.getLogger(KurentoHandler.class);
 	public static final String PARAM_ICE = "iceServers";
@@ -88,20 +91,30 @@ public class KurentoHandler {
 	private final ScheduledExecutorService kmsRecheckScheduler = Executors.newScheduledThreadPool(1);
 	public static final String KURENTO_TYPE = "kurento";
 	private static int FLOWOUT_TIMEOUT_SEC = 5;
-	private long checkTimeout = 120000; //ms
-	private long objCheckTimeout = 200; //ms
-	private int watchThreadCount = 10;
+	@Value("${kurento.ws.url}")
 	private String kurentoWsUrl;
+	@Value("${kurento.turn.url}")
 	private String turnUrl;
+	@Value("${kurento.turn.user}")
 	private String turnUser;
+	@Value("${kurento.turn.secret}")
 	private String turnSecret;
+	@Value("${kurento.turn.mode}")
 	private String turnMode;
+	@Value("${kurento.turn.ttl}")
 	private int turnTtl = 60; //minutes
+	@Value("${kurento.check.timeout}")
+	private long checkTimeout = 120000; //ms
+	@Value("${kurento.object.check.timeout}")
+	private long objCheckTimeout = 200; //ms
+	@Value("${kurento.watch.thread.count}")
+	private int watchThreadCount = 10;
+	@Value("${kurento.kuid}")
+	private String kuid;
 	private KurentoClient client;
 	private final AtomicBoolean connected = new AtomicBoolean(false);
-	private String kuid;
-	private final Set<String> ignoredKuids = new HashSet<>();
 	private final Map<Long, KRoom> rooms = new ConcurrentHashMap<>();
+	private final Set<String> ignoredKuids = new HashSet<>();
 	private Runnable check;
 
 	@Autowired
@@ -379,56 +392,6 @@ public class KurentoHandler {
 		return kuid;
 	}
 
-	public void setKuid(String kuid) {
-		this.kuid = kuid;
-	}
-
-	public void setIgnoredKuids(String ignoredKuids) {
-		if (!Strings.isEmpty(ignoredKuids)) {
-			this.ignoredKuids.addAll(List.of(ignoredKuids.split("[, ]")));
-		}
-	}
-
-	public void setCheckTimeout(long checkTimeout) {
-		this.checkTimeout = checkTimeout;
-	}
-
-	public void setObjCheckTimeout(long objCheckTimeout) {
-		this.objCheckTimeout = objCheckTimeout;
-	}
-
-	public void setWatchThreadCount(int watchThreadCount) {
-		this.watchThreadCount = watchThreadCount;
-	}
-
-	public void setKurentoWsUrl(String kurentoWsUrl) {
-		this.kurentoWsUrl = kurentoWsUrl;
-	}
-
-	public void setTurnUrl(String turnUrl) {
-		this.turnUrl = turnUrl;
-	}
-
-	public void setTurnUser(String turnUser) {
-		this.turnUser = turnUser;
-	}
-
-	public void setTurnSecret(String turnSecret) {
-		this.turnSecret = turnSecret;
-	}
-
-	public void setTurnMode(String turnMode) {
-		this.turnMode = turnMode;
-	}
-
-	public void setTurnTtl(int turnTtl) {
-		this.turnTtl = turnTtl;
-	}
-
-	public void setFlowoutTimeout(int timeout) {
-		FLOWOUT_TIMEOUT_SEC = timeout;
-	}
-
 	IApplication getApp() {
 		return app;
 	}
@@ -445,6 +408,18 @@ public class KurentoHandler {
 		return FLOWOUT_TIMEOUT_SEC;
 	}
 
+	@Value("${kurento.flowout.timeout}")
+	private void setFlowoutTimeout(int timeout) {
+		FLOWOUT_TIMEOUT_SEC = timeout;
+	}
+
+	@Value("${kurento.ignored.kuids}")
+	private void setIgnoredKuids(String ignoredKuids) {
+		if (!Strings.isEmpty(ignoredKuids)) {
+			this.ignoredKuids.addAll(List.of(ignoredKuids.split("[, ]")));
+		}
+	}
+
 	private class KWatchDogCreate implements EventListener<ObjectCreatedEvent> {
 		private ScheduledExecutorService scheduler;
 
diff --git a/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/StreamProcessor.java b/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/StreamProcessor.java
index 7abacce..27fbe41 100644
--- a/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/StreamProcessor.java
+++ b/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/StreamProcessor.java
@@ -306,10 +306,12 @@ public class StreamProcessor implements IStreamProcessor {
 		}
 		KRoom room = kHandler.getRoom(roomId);
 		if (room.isSharing()) {
-			List<StreamDesc> streams = cm.listByRoom(roomId).parallelStream()
+			if (cm.streamByRoom(roomId)
 					.flatMap(c -> c.getStreams().stream())
-					.filter(sd -> StreamType.SCREEN == sd.getType()).collect(Collectors.toList());
-			if (streams.isEmpty()) {
+					.filter(sd -> StreamType.SCREEN == sd.getType())
+					.findAny()
+					.isEmpty())
+			{
 				log.info("No more screen streams in the room, stopping sharing");
 				room.stopSharing();
 				if (Room.Type.INTERVIEW != room.getType() && room.isRecording()) {
@@ -319,10 +321,11 @@ public class StreamProcessor implements IStreamProcessor {
 			}
 		}
 		if (room.isRecording()) {
-			List<StreamDesc> streams = cm.listByRoom(roomId).parallelStream()
+			if (cm.streamByRoom(roomId)
 					.flatMap(c -> c.getStreams().stream())
-					.collect(Collectors.toList());
-			if (streams.isEmpty()) {
+					.findAny()
+					.isEmpty())
+			{
 				log.info("No more streams in the room, stopping recording");
 				room.stopRecording(null);
 			}
diff --git a/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/room/SipDao.java b/openmeetings-core/src/main/java/org/apache/openmeetings/core/sip/SipManager.java
similarity index 84%
rename from openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/room/SipDao.java
rename to openmeetings-core/src/main/java/org/apache/openmeetings/core/sip/SipManager.java
index 5f8b2f5..5ffa17a 100644
--- a/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/room/SipDao.java
+++ b/openmeetings-core/src/main/java/org/apache/openmeetings/core/sip/SipManager.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.openmeetings.db.dao.room;
+package org.apache.openmeetings.core.sip;
 
 import static javax.sip.message.Request.INVITE;
 import static javax.sip.message.Request.REGISTER;
@@ -24,6 +24,8 @@ import static javax.sip.message.Response.OK;
 import static javax.sip.message.Response.RINGING;
 import static javax.sip.message.Response.TRYING;
 import static javax.sip.message.Response.UNAUTHORIZED;
+import static org.apache.openmeetings.util.OmFileHelper.SIP_USER_ID;
+import static org.apache.openmeetings.util.OpenmeetingsVariables.isSipEnabled;
 
 import java.text.ParseException;
 import java.util.List;
@@ -31,6 +33,7 @@ import java.util.Properties;
 import java.util.Random;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.Consumer;
+import java.util.function.Function;
 
 import javax.annotation.PostConstruct;
 import javax.sip.ClientTransaction;
@@ -54,6 +57,9 @@ import javax.sip.message.Request;
 import javax.sip.message.Response;
 
 import org.apache.openmeetings.db.entity.room.Room;
+import org.apache.openmeetings.db.entity.user.User;
+import org.apache.openmeetings.db.manager.ISipManager;
+import org.apache.openmeetings.util.OmFileHelper;
 import org.asteriskjava.manager.DefaultManagerConnection;
 import org.asteriskjava.manager.ManagerConnection;
 import org.asteriskjava.manager.ManagerConnectionFactory;
@@ -70,7 +76,7 @@ import org.asteriskjava.manager.response.ManagerError;
 import org.asteriskjava.manager.response.ManagerResponse;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
 import gov.nist.javax.sip.DialogTimeoutEvent;
@@ -82,8 +88,8 @@ import gov.nist.javax.sip.clientauthutils.UserCredentials;
 import gov.nist.javax.sip.stack.NioMessageProcessorFactory;
 
 @Service
-public class SipDao implements SipListenerExt {
-	private static final Logger log = LoggerFactory.getLogger(SipDao.class);
+public class SipManager implements ISipManager, SipListenerExt {
+	private static final Logger log = LoggerFactory.getLogger(SipManager.class);
 	public static final String ASTERISK_OM_FAMILY = "openmeetings";
 	public static final String ASTERISK_OM_KEY = "rooms";
 	public static final String SIP_FIRST_NAME = "SIP Transport";
@@ -93,6 +99,28 @@ public class SipDao implements SipListenerExt {
 		return t -> {};
 	}
 
+	@Value("${sip.hostname}")
+	private String sipHostname;
+	@Value("${sip.manager.port}")
+	private int managerPort;
+	@Value("${sip.manager.user}")
+	private String managerUser;
+	@Value("${sip.manager.password}")
+	private String managerPass;
+	@Value("${sip.manager.timeout}")
+	private long managerTimeout;
+
+	@Value("${sip.ws.local.port}")
+	private int localWsPort = 6666;
+	@Value("${sip.ws.local.host}")
+	private String localWsHost;
+	@Value("${sip.ws.remote.port}")
+	private int wsPort;
+	@Value("${sip.ws.remote.user}")
+	private String omSipUser;
+	@Value("${sip.ws.remote.password}")
+	private String omSipPasswd;
+
 	private final AtomicLong cseq = new AtomicLong();
 	private final Random rnd = new Random();
 
@@ -106,18 +134,16 @@ public class SipDao implements SipListenerExt {
 	private AddressFactory addressFactory;
 	private ContactHeader contactHeader;
 	private ManagerConnectionFactory factory;
-
-	@Autowired
-	private SipConfig config;
+	private String sipUserPicture;
 
 	@PostConstruct
 	public void init() throws Exception {
-		if (config.getSipHostname() != null) {
+		if (sipHostname != null) {
 			factory = new ManagerConnectionFactory(
-					config.getSipHostname()
-					, config.getManagerPort()
-					, config.getManagerUser()
-					, config.getManagerPass());
+					sipHostname
+					, managerPort
+					, managerUser
+					, managerPass);
 			final SipFactory sipFactory = SipFactory.getInstance();
 			sipFactory.setPathName("gov.nist");
 
@@ -134,14 +160,14 @@ public class SipDao implements SipListenerExt {
 			headerFactory = sipFactory.createHeaderFactory();
 			addressFactory = sipFactory.createAddressFactory();
 			final ListeningPoint listeningPoint = sipStack.createListeningPoint(
-					config.getLocalWsHost()
-					, config.getLocalWsPort()
+					localWsHost
+					, localWsPort
 					, SIP_TRANSPORT);
 			sipProvider = sipStack.createSipProvider(listeningPoint);
 			sipProvider.addSipListener(this);
-			Address contact = createAddr(config.getOmSipUser(), config.getLocalWsHost(), uri -> {
+			Address contact = createAddr(omSipUser, localWsHost, uri -> {
 				try {
-					uri.setPort(config.getLocalWsPort());
+					uri.setPort(localWsPort);
 					uri.setTransportParam(SIP_TRANSPORT);
 				} catch (ParseException e) {
 					log.error("fail to create contact address", e);
@@ -153,10 +179,10 @@ public class SipDao implements SipListenerExt {
 
 	private ManagerConnection getConnection() {
 		DefaultManagerConnection con = (DefaultManagerConnection)factory.createManagerConnection();
-		con.setDefaultEventTimeout(config.getManagerTimeout());
-		con.setDefaultResponseTimeout(config.getManagerTimeout());
-		con.setSocketReadTimeout((int)config.getManagerTimeout());
-		con.setSocketTimeout((int)config.getManagerTimeout());
+		con.setDefaultEventTimeout(managerTimeout);
+		con.setDefaultResponseTimeout(managerTimeout);
+		con.setSocketReadTimeout((int)managerTimeout);
+		con.setSocketTimeout((int)managerTimeout);
 		return con;
 	}
 
@@ -228,6 +254,7 @@ public class SipDao implements SipListenerExt {
 		return pin;
 	}
 
+	@Override
 	public void update(String confno, String pin) {
 		delete(confno);
 		DbPutAction da = new DbPutAction(ASTERISK_OM_FAMILY, getKey(confno), pin);
@@ -239,6 +266,7 @@ public class SipDao implements SipListenerExt {
 		exec(da);
 	}
 
+	@Override
 	public void delete(String confno) {
 		DbDelAction da = new DbDelAction(ASTERISK_OM_FAMILY, getKey(confno));
 		exec(da);
@@ -251,7 +279,7 @@ public class SipDao implements SipListenerExt {
 		ConfbridgeListAction da = new ConfbridgeListAction(confno);
 		ResponseEvents r = execEvent(da);
 		if (r != null) {
-			log.debug("SipDao::countUsers size == {}", r.getEvents().size());
+			log.debug("SipManager::countUsers size == {}", r.getEvents().size());
 			// "- 1" here means: ListComplete event
 			return r.getEvents().size() - 1;
 		}
@@ -278,11 +306,29 @@ public class SipDao implements SipListenerExt {
 		oa.setContext("rooms-out");
 		oa.setExten(number);
 		oa.setPriority(1);
-		oa.setTimeout(config.getManagerTimeout());
+		oa.setTimeout(managerTimeout);
 
 		exec(oa);
 	}
 
+	public void setUserPicture(Function<User, String> pictureCreator) {
+		User u = new User();
+		u.setId(SIP_USER_ID);
+		sipUserPicture = pictureCreator.apply(u);
+	}
+
+	public User getSipUser(Room r) {
+		if (factory == null || !isSipEnabled() || !r.isSipEnabled()) {
+			return null;
+		}
+		User u = new User();
+		u.setId(OmFileHelper.SIP_USER_ID);
+		u.setFirstname(SIP_FIRST_NAME);
+		u.setLogin(SIP_USER_NAME);
+		u.setPictureUri(sipUserPicture);
+		return u;
+	}
+
 	@Override
 	public void processDialogTerminated(DialogTerminatedEvent evt) {
 		log.error("processDialogTerminated: \n{}", evt);
@@ -355,7 +401,7 @@ public class SipDao implements SipListenerExt {
 	}
 
 	private Address createAddr(String user) {
-		return createAddr(user, config.getSipHostname(), noop());
+		return createAddr(user, sipHostname, noop());
 	}
 
 	private Address createAddr(String user, String host, Consumer<SipUri> cons) {
@@ -373,11 +419,11 @@ public class SipDao implements SipListenerExt {
 
 	private void sendRequest(String method, String to, Consumer<SipUri> uriCons, Consumer<Request> reqCons) throws Exception {
 		SipUri uri = new SipUri();
-		uri.setHost(config.getSipHostname());
-		uri.setPort(config.getWsPort());
+		uri.setHost(sipHostname);
+		uri.setPort(wsPort);
 		uri.setTransportParam(SIP_TRANSPORT);
 		uri.setMethodParam("GET");
-		uri.setHeader("Host", config.getSipHostname());
+		uri.setHeader("Host", sipHostname);
 		uri.setHeader("Location", "/ws");
 		uriCons.accept(uri);
 
@@ -386,9 +432,9 @@ public class SipDao implements SipListenerExt {
 				, method
 				, sipProvider.getNewCallId()
 				, headerFactory.createCSeqHeader(cseq.incrementAndGet(), method)
-				, headerFactory.createFromHeader(createAddr(config.getOmSipUser()), tag)
+				, headerFactory.createFromHeader(createAddr(omSipUser), tag)
 				, headerFactory.createToHeader(createAddr(to), null)
-				, List.of(headerFactory.createViaHeader(config.getLocalWsHost(), config.getLocalWsPort(), SIP_TRANSPORT, branch))
+				, List.of(headerFactory.createViaHeader(localWsHost, localWsPort, SIP_TRANSPORT, branch))
 				, headerFactory.createMaxForwardsHeader(70));
 		request.addHeader(contactHeader);
 		request.addHeader(headerFactory.createExpiresHeader(600));
@@ -407,12 +453,12 @@ public class SipDao implements SipListenerExt {
 		AuthenticationHelper helper = sipStack.getAuthenticationHelper((trans, s) -> new UserCredentials() {
 			@Override
 			public String getUserName() {
-				return config.getOmSipUser();
+				return omSipUser;
 			}
 
 			@Override
 			public String getPassword() {
-				return config.getOmSipPasswd();
+				return omSipPasswd;
 			}
 
 			@Override
@@ -435,7 +481,7 @@ public class SipDao implements SipListenerExt {
 	private void register() throws Exception {
 		sendRequest(
 				REGISTER
-				, config.getOmSipUser()
+				, omSipUser
 				, noop()
 				, req -> {
 					try {
diff --git a/openmeetings-core/src/main/java/org/apache/openmeetings/core/util/ChatWebSocketHelper.java b/openmeetings-core/src/main/java/org/apache/openmeetings/core/util/ChatWebSocketHelper.java
index 61d480f..9f733f2 100644
--- a/openmeetings-core/src/main/java/org/apache/openmeetings/core/util/ChatWebSocketHelper.java
+++ b/openmeetings-core/src/main/java/org/apache/openmeetings/core/util/ChatWebSocketHelper.java
@@ -18,6 +18,7 @@
  */
 package org.apache.openmeetings.core.util;
 
+import static org.apache.openmeetings.core.util.WebSocketHelper.alwaysTrue;
 import static org.apache.openmeetings.core.util.WebSocketHelper.doSend;
 import static org.apache.openmeetings.core.util.WebSocketHelper.publish;
 
@@ -148,7 +149,7 @@ public class ChatWebSocketHelper {
 		if (publish) {
 			publish(new WsMessageChat2All(m, msg));
 		}
-		WebSocketHelper.send(a -> ((IApplication)a).getBean(IClientManager.class).list()
-				, (t, c) -> doSend(t, c, msg, (o, cm) -> setDates(o, m, c.getUser(), false), "all"), null);
+		WebSocketHelper.send(a -> ((IApplication)a).getBean(IClientManager.class).stream()
+				, (t, c) -> doSend(t, c, msg, (o, cm) -> setDates(o, m, c.getUser(), false), "all"), alwaysTrue());
 	}
 }
diff --git a/openmeetings-core/src/main/java/org/apache/openmeetings/core/util/WebSocketHelper.java b/openmeetings-core/src/main/java/org/apache/openmeetings/core/util/WebSocketHelper.java
index 10a2de7..16c3698 100644
--- a/openmeetings-core/src/main/java/org/apache/openmeetings/core/util/WebSocketHelper.java
+++ b/openmeetings-core/src/main/java/org/apache/openmeetings/core/util/WebSocketHelper.java
@@ -21,12 +21,12 @@ package org.apache.openmeetings.core.util;
 import static org.apache.openmeetings.util.OpenmeetingsVariables.getWicketApplicationName;
 
 import java.io.IOException;
-import java.util.Collection;
 import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.stream.Stream;
 
 import org.apache.openmeetings.IApplication;
 import org.apache.openmeetings.core.util.ws.WsMessageAll;
@@ -54,6 +54,9 @@ import com.github.openjson.JSONObject;
 
 public class WebSocketHelper {
 	private static final Logger log = LoggerFactory.getLogger(WebSocketHelper.class);
+	public static final <T> Predicate<T> alwaysTrue() {
+		return x -> true;
+	}
 
 	private WebSocketHelper() {
 		// denied
@@ -139,7 +142,7 @@ public class WebSocketHelper {
 			publish(new WsMessageRoomMsg(m));
 		}
 		log.trace("Sending WebSocket message to room: {} {}", m.getType(), m instanceof TextRoomMessage ? ((TextRoomMessage)m).getText() : "");
-		sendRoom(m.getRoomId(), (t, c) -> t.sendMessage(m), null);
+		sendRoom(m.getRoomId(), (t, c) -> t.sendMessage(m), alwaysTrue());
 	}
 
 	public static void sendServer(final RoomMessage m) {
@@ -161,7 +164,7 @@ public class WebSocketHelper {
 		if (publish) {
 			publish(new WsMessageRoom(roomId, m));
 		}
-		sendRoom(roomId, m, null, null);
+		sendRoom(roomId, m, alwaysTrue(), null);
 	}
 
 	public static void sendRoomOthers(final Long roomId, final String uid, final JSONObject m) {
@@ -183,8 +186,8 @@ public class WebSocketHelper {
 		if (publish) {
 			publish(new WsMessageUser(userId, m));
 		}
-		send(a -> ((IApplication)a).getBean(IClientManager.class).listByUser(userId)
-				, (t, c) -> doSend(t, c, m, func, "user"), null);
+		send(a -> ((IApplication)a).getBean(IClientManager.class).listByUser(userId).stream()
+				, (t, c) -> doSend(t, c, m, func, "user"), alwaysTrue());
 	}
 
 	public static void sendAll(final String m) {
@@ -239,11 +242,11 @@ public class WebSocketHelper {
 	}
 
 	private static void sendRoom(final Long roomId, BiConsumer<IWebSocketConnection, Client> consumer, Predicate<Client> check) {
-		send(a -> ((IApplication)a).getBean(IClientManager.class).listByRoom(roomId), consumer, check);
+		send(a -> ((IApplication)a).getBean(IClientManager.class).streamByRoom(roomId), consumer, check);
 	}
 
 	static void send(
-			final Function<Application, Collection<Client>> func
+			final Function<Application, Stream<Client>> func
 			, BiConsumer<IWebSocketConnection, Client> consumer
 			, Predicate<Client> check)
 	{
@@ -255,14 +258,14 @@ public class WebSocketHelper {
 			WebSocketSettings settings = WebSocketSettings.Holder.get(app);
 			IWebSocketConnectionRegistry reg = settings.getConnectionRegistry();
 			Executor executor = settings.getWebSocketPushMessageExecutor();
-			for (Client c : func.apply(app)) {
-				if (check == null || check.test(c)) {
-					final IWebSocketConnection wc = reg.getConnection(app, c.getSessionId(), new PageIdKey(c.getPageId()));
-					if (wc != null && wc.isOpen()) {
-						executor.run(() -> consumer.accept(wc, c));
-					}
-				}
-			}
+			func.apply(app)
+					.filter(check)
+					.forEach(c -> {
+						final IWebSocketConnection wc = reg.getConnection(app, c.getSessionId(), new PageIdKey(c.getPageId()));
+						if (wc != null && wc.isOpen()) {
+							executor.run(() -> consumer.accept(wc, c));
+						}
+					});
 		}).start();
 	}
 }
diff --git a/openmeetings-db/pom.xml b/openmeetings-db/pom.xml
index cd6d82a..16562ac 100644
--- a/openmeetings-db/pom.xml
+++ b/openmeetings-db/pom.xml
@@ -73,11 +73,6 @@
 			<version>${spring.version}</version>
 		</dependency>
 		<dependency>
-			<groupId>org.asteriskjava</groupId>
-			<artifactId>asterisk-java</artifactId>
-			<version>${asterisk-java.version}</version>
-		</dependency>
-		<dependency>
 			<groupId>org.apache.commons</groupId>
 			<artifactId>commons-dbcp2</artifactId>
 			<version>${commons-dbcp.version}</version>
@@ -109,10 +104,6 @@
 			<version>${mssql.version}</version>
 		</dependency>
 		<dependency>
-			<groupId>javax.sip</groupId>
-			<artifactId>jain-sip-ri</artifactId>
-		</dependency>
-		<dependency>
 			<groupId>org.apache.openmeetings</groupId>
 			<artifactId>openmeetings-util</artifactId>
 			<version>${project.version}</version>
diff --git a/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/calendar/AppointmentDao.java b/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/calendar/AppointmentDao.java
index 988bc2e..d5b52f8 100644
--- a/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/calendar/AppointmentDao.java
+++ b/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/calendar/AppointmentDao.java
@@ -37,13 +37,13 @@ import javax.persistence.TypedQuery;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.openmeetings.db.dao.IDataProviderDao;
 import org.apache.openmeetings.db.dao.basic.ConfigurationDao;
-import org.apache.openmeetings.db.dao.room.IInvitationManager;
 import org.apache.openmeetings.db.dao.room.RoomDao;
 import org.apache.openmeetings.db.dto.calendar.AppointmentDTO;
 import org.apache.openmeetings.db.entity.calendar.Appointment;
 import org.apache.openmeetings.db.entity.calendar.Appointment.Reminder;
 import org.apache.openmeetings.db.entity.calendar.MeetingMember;
 import org.apache.openmeetings.db.entity.room.Invitation.MessageType;
+import org.apache.openmeetings.db.manager.IInvitationManager;
 import org.apache.openmeetings.db.entity.room.Room;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
diff --git a/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/room/RoomDao.java b/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/room/RoomDao.java
index 02b84f5..696959b 100644
--- a/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/room/RoomDao.java
+++ b/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/room/RoomDao.java
@@ -46,6 +46,7 @@ import org.apache.openmeetings.db.entity.log.ConferenceLog;
 import org.apache.openmeetings.db.entity.room.Room;
 import org.apache.openmeetings.db.entity.room.Room.RoomElement;
 import org.apache.openmeetings.db.entity.room.Room.Type;
+import org.apache.openmeetings.db.manager.ISipManager;
 import org.apache.openmeetings.db.entity.room.RoomFile;
 import org.apache.openmeetings.db.entity.room.RoomGroup;
 import org.apache.openmeetings.db.util.DaoHelper;
@@ -70,7 +71,7 @@ public class RoomDao implements IGroupAdminDataProviderDao<Room> {
 	@Autowired
 	private ConfigurationDao cfgDao;
 	@Autowired
-	private SipDao sipDao;
+	private ISipManager sipManager;
 	@Autowired
 	private UserDao userDao;
 
@@ -213,9 +214,9 @@ public class RoomDao implements IGroupAdminDataProviderDao<Room> {
 			if (sipNumber != null && !sipNumber.equals(entity.getConfno())) {
 				entity.setConfno(sipNumber);
 			}
-			sipDao.update(sipNumber, entity.getPin());
+			sipManager.update(sipNumber, entity.getPin());
 		} else {
-			sipDao.delete(entity.getConfno());
+			sipManager.delete(entity.getConfno());
 			entity.setConfno(null);
 			entity.setPin(null);
 		}
diff --git a/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/room/SipConfig.java b/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/room/SipConfig.java
deleted file mode 100644
index e1a5646..0000000
--- a/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/room/SipConfig.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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 org.apache.openmeetings.db.dao.room;
-
-public class SipConfig {
-	private String sipHostname;
-	private int managerPort;
-	private String managerUser;
-	private String managerPass;
-	private long managerTimeout;
-
-	private int localWsPort = 6666;
-	private String localWsHost;
-	private int wsPort;
-	private String omSipUser;
-	private String omSipPasswd;
-
-	private String uid; //FIXME TODO is this still required ?!
-
-	public String getSipHostname() {
-		return sipHostname;
-	}
-
-	public void setSipHostname(String sipHostname) {
-		this.sipHostname = sipHostname;
-	}
-
-	public int getManagerPort() {
-		return managerPort;
-	}
-
-	public void setManagerPort(int managerPort) {
-		this.managerPort = managerPort;
-	}
-
-	public String getManagerUser() {
-		return managerUser;
-	}
-
-	public void setManagerUser(String managerUser) {
-		this.managerUser = managerUser;
-	}
-
-	public String getManagerPass() {
-		return managerPass;
-	}
-
-	public void setManagerPass(String managerPass) {
-		this.managerPass = managerPass;
-	}
-
-	public long getManagerTimeout() {
-		return managerTimeout;
-	}
-
-	public void setManagerTimeout(long managerTimeout) {
-		this.managerTimeout = managerTimeout;
-	}
-
-	public int getLocalWsPort() {
-		return localWsPort;
-	}
-
-	public void setLocalWsPort(int localWsPort) {
-		this.localWsPort = localWsPort;
-	}
-
-	public String getLocalWsHost() {
-		return localWsHost;
-	}
-
-	public void setLocalWsHost(String localWsHost) {
-		this.localWsHost = localWsHost;
-	}
-
-	public int getWsPort() {
-		return wsPort;
-	}
-
-	public void setWsPort(int wsPort) {
-		this.wsPort = wsPort;
-	}
-
-	public String getOmSipUser() {
-		return omSipUser;
-	}
-
-	public void setOmSipUser(String omSipUser) {
-		this.omSipUser = omSipUser;
-	}
-
-	public String getOmSipPasswd() {
-		return omSipPasswd;
-	}
-
-	public void setOmSipPasswd(String omSipPasswd) {
-		this.omSipPasswd = omSipPasswd;
-	}
-
-	public String getUid() {
-		return uid;
-	}
-
-	public void setUid(String uid) {
-		this.uid = uid;
-	}
-}
diff --git a/openmeetings-db/src/main/java/org/apache/openmeetings/db/entity/basic/Client.java b/openmeetings-db/src/main/java/org/apache/openmeetings/db/entity/basic/Client.java
index cdf463b..0304452 100644
--- a/openmeetings-db/src/main/java/org/apache/openmeetings/db/entity/basic/Client.java
+++ b/openmeetings-db/src/main/java/org/apache/openmeetings/db/entity/basic/Client.java
@@ -19,6 +19,7 @@
 package org.apache.openmeetings.db.entity.basic;
 
 import static java.util.UUID.randomUUID;
+import static org.apache.openmeetings.util.OmFileHelper.SIP_USER_ID;
 
 import java.io.Serializable;
 import java.util.ArrayList;
@@ -129,6 +130,10 @@ public class Client implements IDataProviderEntity, IWsClient {
 		return sid;
 	}
 
+	public boolean isSip() {
+		return SIP_USER_ID.equals(getUserId());
+	}
+
 	public void clear() {
 		activities.clear();
 		rights.clear();
diff --git a/openmeetings-db/src/main/java/org/apache/openmeetings/db/manager/IClientManager.java b/openmeetings-db/src/main/java/org/apache/openmeetings/db/manager/IClientManager.java
index 24a1498..0e894da 100644
--- a/openmeetings-db/src/main/java/org/apache/openmeetings/db/manager/IClientManager.java
+++ b/openmeetings-db/src/main/java/org/apache/openmeetings/db/manager/IClientManager.java
@@ -19,7 +19,7 @@
 package org.apache.openmeetings.db.manager;
 
 import java.util.Collection;
-import java.util.List;
+import java.util.stream.Stream;
 
 import org.apache.openmeetings.db.entity.basic.Client;
 
@@ -27,8 +27,8 @@ public interface IClientManager {
 	Client get(String uid);
 	Client getBySid(String sid);
 	String uidBySid(String sid);
-	List<Client> list();
-	List<Client> listByRoom(Long roomId);
+	Stream<Client> stream();
+	Stream<Client> streamByRoom(Long roomId);
 	Collection<Client> listByUser(Long userId);
 	Client update(Client c);
 	void exit(Client c);
diff --git a/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/room/IInvitationManager.java b/openmeetings-db/src/main/java/org/apache/openmeetings/db/manager/IInvitationManager.java
similarity index 97%
rename from openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/room/IInvitationManager.java
rename to openmeetings-db/src/main/java/org/apache/openmeetings/db/manager/IInvitationManager.java
index 2fd0c34..44f3f65 100644
--- a/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/room/IInvitationManager.java
+++ b/openmeetings-db/src/main/java/org/apache/openmeetings/db/manager/IInvitationManager.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.openmeetings.db.dao.room;
+package org.apache.openmeetings.db.manager;
 
 import java.util.Date;
 
diff --git a/openmeetings-db/src/main/java/org/apache/openmeetings/db/manager/IClientManager.java b/openmeetings-db/src/main/java/org/apache/openmeetings/db/manager/ISipManager.java
similarity index 69%
copy from openmeetings-db/src/main/java/org/apache/openmeetings/db/manager/IClientManager.java
copy to openmeetings-db/src/main/java/org/apache/openmeetings/db/manager/ISipManager.java
index 24a1498..b94263a 100644
--- a/openmeetings-db/src/main/java/org/apache/openmeetings/db/manager/IClientManager.java
+++ b/openmeetings-db/src/main/java/org/apache/openmeetings/db/manager/ISipManager.java
@@ -18,18 +18,12 @@
  */
 package org.apache.openmeetings.db.manager;
 
-import java.util.Collection;
-import java.util.List;
-
-import org.apache.openmeetings.db.entity.basic.Client;
+/**
+ * this interface is required to use SipMagager from openmeetings-core
+ */
+public interface ISipManager {
+	public static final String SIP_FIRST_NAME = "SIP Transport";
 
-public interface IClientManager {
-	Client get(String uid);
-	Client getBySid(String sid);
-	String uidBySid(String sid);
-	List<Client> list();
-	List<Client> listByRoom(Long roomId);
-	Collection<Client> listByUser(Long userId);
-	Client update(Client c);
-	void exit(Client c);
+	void update(String confno, String pin);
+	void delete(String confno);
 }
diff --git a/openmeetings-db/src/main/java/org/apache/openmeetings/db/util/ApplicationHelper.java b/openmeetings-db/src/main/java/org/apache/openmeetings/db/util/ApplicationHelper.java
index 38ef520..b8d7919 100644
--- a/openmeetings-db/src/main/java/org/apache/openmeetings/db/util/ApplicationHelper.java
+++ b/openmeetings-db/src/main/java/org/apache/openmeetings/db/util/ApplicationHelper.java
@@ -116,13 +116,17 @@ public class ApplicationHelper {
 		}
 	}
 
-	public static IApplication ensureApplication(Long langId) {
-		IApplication a = ensureApplication();
+	public static void ensureRequestCycle(IApplication a) {
 		if (ThreadContext.getRequestCycle() == null) {
 			ServletWebRequest req = new ServletWebRequest(new MockHttpServletRequest((Application)a, new MockHttpSession(a.getServletContext()), a.getServletContext()), "");
 			RequestCycleContext rctx = new RequestCycleContext(req, new MockWebResponse(), a.getRootRequestMapper(), a.getExceptionMapperProvider().get());
 			ThreadContext.setRequestCycle(new RequestCycle(rctx));
 		}
+	}
+
+	public static IApplication ensureApplication(Long langId) {
+		IApplication a = ensureApplication();
+		ensureRequestCycle(a);
 		if (ThreadContext.getSession() == null) {
 			WebSession s = WebSession.get();
 			if (langId > 0) {
diff --git a/openmeetings-db/src/main/java/org/apache/openmeetings/db/util/ws/RoomMessage.java b/openmeetings-db/src/main/java/org/apache/openmeetings/db/util/ws/RoomMessage.java
index 85937b7..9864bc2 100644
--- a/openmeetings-db/src/main/java/org/apache/openmeetings/db/util/ws/RoomMessage.java
+++ b/openmeetings-db/src/main/java/org/apache/openmeetings/db/util/ws/RoomMessage.java
@@ -19,7 +19,7 @@
 package org.apache.openmeetings.db.util.ws;
 
 import static java.util.UUID.randomUUID;
-import static org.apache.openmeetings.db.dao.room.SipDao.SIP_FIRST_NAME;
+import static org.apache.openmeetings.db.manager.ISipManager.SIP_FIRST_NAME;
 import static org.apache.openmeetings.util.OmFileHelper.SIP_USER_ID;
 
 import java.util.Date;
diff --git a/openmeetings-install/src/main/java/org/apache/openmeetings/installation/ImportInitvalues.java b/openmeetings-install/src/main/java/org/apache/openmeetings/installation/ImportInitvalues.java
index 718078e..f987e1d 100644
--- a/openmeetings-install/src/main/java/org/apache/openmeetings/installation/ImportInitvalues.java
+++ b/openmeetings-install/src/main/java/org/apache/openmeetings/installation/ImportInitvalues.java
@@ -125,10 +125,10 @@ import java.util.Date;
 import java.util.List;
 import java.util.function.Consumer;
 
+import org.apache.openmeetings.core.sip.SipManager;
 import org.apache.openmeetings.db.dao.basic.ConfigurationDao;
 import org.apache.openmeetings.db.dao.label.LabelDao;
 import org.apache.openmeetings.db.dao.room.RoomDao;
-import org.apache.openmeetings.db.dao.room.SipDao;
 import org.apache.openmeetings.db.dao.server.OAuth2Dao;
 import org.apache.openmeetings.db.dao.user.GroupDao;
 import org.apache.openmeetings.db.dao.user.UserDao;
@@ -170,7 +170,7 @@ public class ImportInitvalues {
 	@Autowired
 	private UserDao userDao;
 	@Autowired
-	private SipDao sipDao;
+	private SipManager sipDao;
 	@Autowired
 	private OAuth2Dao oauthDao;
 	@Autowired
diff --git a/openmeetings-server/src/site/markdown/AsteriskIntegration.md b/openmeetings-server/src/site/markdown/AsteriskIntegration.md
index 2fd9105..554584f 100644
--- a/openmeetings-server/src/site/markdown/AsteriskIntegration.md
+++ b/openmeetings-server/src/site/markdown/AsteriskIntegration.md
@@ -165,7 +165,7 @@ Modify `/etc/asterisk/extensions.conf`
 ; If you do not receive an output with that resembles openmeetings/rooms/400## where “##” will equal
 ; the extension assigned when you created your room
 ; If you do not receive the above output check your parameters in
-; /opt/om/webapps/openmeetings/WEB-INF/classes/applicationContext.xml
+; /opt/om/webapps/openmeetings/WEB-INF/classes/openmeetings.properties
 ; Go back into the Administrator Panel and remove the PIN number in each room save the record with
 ; no PIN number and then re-enter the pin again resave the record.
 ; *****************************************************
@@ -246,12 +246,9 @@ write = all
 ```
 
 Update OpenMeetings with credentials for Asterisk manager.
-Modify `/opt/om/webapps/openmeetings/WEB-INF/classes/applicationContext.xml`
+Modify `/opt/om/webapps/openmeetings/WEB-INF/classes/openmeetings.properties`
 
-find **&lt;bean class="org.apache.openmeetings.db.dao.room.SipConfig"&gt;**
-uncomment its properties and set it to your custom values.
-
-set value for `uid` property to unique secret value (can be generated here <a href="https://www.uuidtools.com">https://www.uuidtools.com</a>)
+find all properties start with `sip.` and set it to your custom values.
 
 <p style="font-size: larger; color: blue;">
 	IMPORTANT: this step should be done <strong>BEFORE</strong> system install/restore
diff --git a/openmeetings-server/src/site/markdown/InstallMediaServer.md b/openmeetings-server/src/site/markdown/InstallMediaServer.md
index 3034ca7..8ffc72e 100644
--- a/openmeetings-server/src/site/markdown/InstallMediaServer.md
+++ b/openmeetings-server/src/site/markdown/InstallMediaServer.md
@@ -13,4 +13,4 @@ Licensed under the Apache License, Version 2.0 (the "License") http://www.apache
 
 ## Specify/Install Turn server
 
-<div class="bd-callout bd-callout-info">Optional step</div>
+<div class="bd-callout bd-callout-warning">Only local installation will work without TURN server</div>
diff --git a/openmeetings-server/src/site/site.xml b/openmeetings-server/src/site/site.xml
index ef599b9..5a27a60 100644
--- a/openmeetings-server/src/site/site.xml
+++ b/openmeetings-server/src/site/site.xml
@@ -47,7 +47,7 @@
 				<item name="REST API Sample" href="/RestAPISample.html" />
 				<item name="Ldap and ADS" href="/LdapAndADS.html" />
 				<item name="OAuth2" href="/oauth2.html" />
-				<item name="VoIP and SIP" href="/voip-sip-integration.html" />
+				<item name="VoIP and SIP" href="/AsteriskIntegration.html" />
 				<item name="Errors table" href="/errorvalues.html" />
 				<item name="CalDAV and Google Calendar integration" href="/CalDAVandGCal.html" />
 				<item name="External Video/Camera" href="/ExternalVideo.html" />
diff --git a/openmeetings-server/src/site/xdoc/PrivacyStatement.xml b/openmeetings-server/src/site/xdoc/PrivacyStatement.xml
index ad91620..319f18b 100644
--- a/openmeetings-server/src/site/xdoc/PrivacyStatement.xml
+++ b/openmeetings-server/src/site/xdoc/PrivacyStatement.xml
@@ -24,7 +24,7 @@
 			<div>
 				To modify privacy statement do the following:
 				<ul>
-					<li>Open <pp>webapps/openmeetings/WEB-INF/classes/org/apache/openmeetings/web/pages/PrivacyPage.html</pp></li>
+					<li>Open <tt>webapps/openmeetings/WEB-INF/classes/org/apache/openmeetings/web/pages/PrivacyPage.html</tt></li>
 					<li>Perform necessary changes</li>
 					<li>restart OM</li>
 				</ul>
@@ -35,11 +35,11 @@
 				To create privacy statement in your language do the following:
 				<ul>
 					<li>
-						Create new file <pp>webapps/openmeetings/WEB-INF/classes/org/apache/openmeetings/web/pages/PrivacyPage_<b>LANG_CODE</b>.html</pp> <br/>
-						<pp>LANG_CODE</pp> should be in format [language code]_[country ISO code], <pp>_[country ISO code]</pp> block is optional<br/>
-						for ex. <pp>de</pp> can be suffix for German, file name will be <pp>webapps/openmeetings/WEB-INF/classes/org/apache/openmeetings/web/pages/PrivacyPage_de.html</pp><br/>
-						<pp>pt</pp> can be suffix for Portuguese, file name will be <pp>webapps/openmeetings/WEB-INF/classes/org/apache/openmeetings/web/pages/PrivacyPage_pt.html</pp><br/>
-						<pp>pt_BR</pp> can be suffix for Brazilian version of Portuguese, file name will be <pp>webapps/openmeetings/WEB-INF/classes/org/apache/openmeetings/web/pages/PrivacyPage_pt_BR.html</pp><br/>
+						Create new file <tt>webapps/openmeetings/WEB-INF/classes/org/apache/openmeetings/web/pages/PrivacyPage_<b>LANG_CODE</b>.html</tt> <br/>
+						<tt>LANG_CODE</tt> should be in format [language code]_[country ISO code], <tt>_[country ISO code]</tt> block is optional<br/>
+						for ex. <tt>de</tt> can be suffix for German, file name will be <tt>webapps/openmeetings/WEB-INF/classes/org/apache/openmeetings/web/pages/PrivacyPage_de.html</tt><br/>
+						<tt>pt</tt> can be suffix for Portuguese, file name will be <tt>webapps/openmeetings/WEB-INF/classes/org/apache/openmeetings/web/pages/PrivacyPage_pt.html</tt><br/>
+						<tt>pt_BR</tt> can be suffix for Brazilian version of Portuguese, file name will be <tt>webapps/openmeetings/WEB-INF/classes/org/apache/openmeetings/web/pages/PrivacyPage_pt_BR.html</tt><br/>
 					</li>
 					<li>Perform translation</li>
 					<li>restart OM</li>
diff --git a/openmeetings-server/src/site/xdoc/voip-sip-integration.xml b/openmeetings-server/src/site/xdoc/voip-sip-integration.xml
deleted file mode 100644
index 85bef4b..0000000
--- a/openmeetings-server/src/site/xdoc/voip-sip-integration.xml
+++ /dev/null
@@ -1,360 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-   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.
- -->
-<document xmlns="http://maven.apache.org/XDOC/2.0"
-		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-		xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
-	<properties>
-		<title>VoIP and SIP Integration</title>
-		<author email="dev@openmeetings.apache.org">Apache OpenMeetings Team</author>
-	</properties>
-	<body>
-		<section name="Not implemented">
-			<div class="bd-callout bd-callout-danger">
-				Please NOTE: this functionality is not yet implemented in 5.0.x
-			</div>
-		</section>
-
-		<section name="VoIP and SIP Integration">
-			<p>
-				There are multiple ways to integrate with VoIP and or SIP.
-				OpenMeetings does not provide out of the box a ready to run VoIP
-				integration / integration to cell phone or usual land lane.
-				The
-				nature of such integrations is that it depends heavily on the
-				infrastructure that you are using and where you would like to
-				integrate OpenMeetings into.
-				<br />
-				<br />
-				It also depends on a number of factors of which OpenMeetings is
-				impossible to set up for you, for example setting up your VoIP
-				server or provide you with a range of telephone numbers reserved for
-				conference calls in your national phone network.
-				Such an integration
-				project is likely to become a consulting job for a
-				telecommunications consultant.
-				<br />
-				<br />
-				To get help on the integration you can contact the
-				<a href="mailing-lists.html">mailing lists</a>
-				or for example somebody from the list of
-				<a href="commercial-support.html">commercial support</a>.
-				<br/><br/>
-			</p>
-		</section>
-		<section name="SIP-Transport Integration">
-			<p>You need Apache OpenMeetings <strong>version 4.0+</strong> to apply this guide!</p>
-			<p>You need Asterisk <strong>version 13+</strong> to apply this guide!</p>
-			<p>Here is instruction how-to set up red5sip transport integration with OpenMeetings on Ubuntu 16.04.</p>
-		</section>
-		<section name="Prerequisites">
-			<div>
-				Run the commands
-				<source>
-<![CDATA[
-sudo apt update && sudo apt upgrade
-]]>
-				</source>
-			</div>
-		</section>
-		<section name="Building and setting up Asterisk">
-			<div>
-				Run the commands
-				<source>
-<![CDATA[
-sudo mkdir /usr/src/asterisk && cd /usr/src/asterisk
-sudo wget http://downloads.asterisk.org/pub/telephony/asterisk/releases/asterisk-13.17.0.tar.gz
-sudo tar -xvzf asterisk-13.17.0.tar.gz
-cd ./asterisk-13.17.0
-sudo make clean
-sudo contrib/scripts/install_prereq install
-sudo ./configure
-sudo make menuconfig
-]]>
-				</source>
-				Make sure you have selected  <tt>Add-ons -> res_config_mysql</tt>, Press F12 to save
-				<source>
-<![CDATA[
-sudo make
-sudo make install
-sudo make samples
-sudo make config
-sudo service asterisk start
-]]>
-				</source>
-			</div>
-		</section>
-		<section name="Configure Asterisk">
-			<div>
-				Enable asterisk MySQL module:<br /><br />
-				Modify "[modules]" section of <tt>/etc/asterisk/modules.conf</tt> as follows:<br />
-				<strong>Add/uncomment the following lines</strong>
-				<source>
-<![CDATA[
-preload => res_config_mysql.so
-]]>
-				</source>
-			</div><br />
-			<div>
-				Configure MySQL module:<br /><br />
-				Set valid data for MySQL in <tt>/etc/asterisk/res_config_mysql.conf</tt> :<br />
-				<strong>Example</strong>
-				<source>
-<![CDATA[
-[general]
-dbhost = 127.0.0.1
-dbname = openmeetings
-dbuser = root
-dbpass =
-dbport = 3306
-dbsock = /var/lib/mysql/mysql.sock
-dbcharset = utf8
-requirements=warn
-]]>
-				</source>
-			</div><br />
-			<div>
-				Modify <tt>/etc/asterisk/sip.conf</tt><br />
-				<strong>Add/uncomment the following line</strong>:<br />
-				<source>
-<![CDATA[
-videosupport=yes
-rtcachefriends=yes
-]]>
-				</source>
-				<strong>Increase maxexpiry value to 43200</strong>:<br />
-				<source>
-<![CDATA[
-maxexpiry=43200
-]]>
-				</source>
-				<strong>Add user for the "SIP Transport"</strong>:<br />
-				<source>
-<![CDATA[
-[red5sip_user]
-type=friend
-secret=12345
-disallow=all
-allow=ulaw
-allow=h263
-host=dynamic
-nat=force_rport,comedia
-context=rooms-red5sip
-]]>
-				</source>
-			</div><br />
-			<div>
-				Add next lines into the <tt>/etc/asterisk/extconfig.conf</tt>:
-				<source>
-<![CDATA[
-[settings]
-sippeers => mysql,general,sipusers
-]]>
-				</source>
-			</div><br />
-			<div>
-				Modify <tt>/etc/asterisk/extensions.conf</tt><br />
-				<strong>Add the following section</strong>:<br />
-				<source>
-<![CDATA[
-; *****************************************************
-; The below dial plan is used to dial into a Openmeetings Conference room
-; The first line DB_EXISTS(openmeetings/room/ does not belong to the openmeetings application
-; but is the name of astDB containing the astDB family/key pair and values
-; To Check if your astDB has been created do the following in a terminal window type the following:
-; asterisk –rx “database show”
-; If you do not receive an output with that resembles openmeetings/rooms/400## where “##” will equal
-; the extension assigned when you created your room
-; If you do not receive the above output check your parameters in
-; /opt/om/webapps/openmeetings/WEB-INF/classes/applicationContext.xml
-; Go back into the Administrator Panel and remove the PIN number in each room save the record with
-; no PIN number and then re-enter the pin again resave the record.
-; *****************************************************
-
-[rooms]
-exten => _400X!,1,GotoIf($[${DB_EXISTS(openmeetings/rooms/${EXTEN})}]?ok:notavail)
-exten => _400X!,n(ok),SET(PIN=${DB(openmeetings/rooms/${EXTEN})})
-exten => _400X!,n,Set(CONFBRIDGE(user,template)=sip_user)
-exten => _400X!,n,Set(CONFBRIDGE(user,pin)=${PIN})
-exten => _400X!,n(ok),Confbridge(${EXTEN},default_bridge,)
-exten => _400X!,n,Hangup
-exten => _400X!,n(notavail),Answer()
-exten => _400X!,n,Playback(invalid)
-exten => _400X!,n,Hangup
-
-[rooms-originate]
-exten => _400X!,1,Confbridge(${EXTEN},default_bridge,sip_user)
-exten => _400X!,n,Hangup
-
-[rooms-out]
-; *****************************************************
-; Extensions for outgoing calls from Openmeetings room.
-; *****************************************************
-
-[rooms-red5sip]
-exten => _400X!,1,GotoIf($[${DB_EXISTS(openmeetings/rooms/${EXTEN})}]?ok:notavail)
-exten => _400X!,n(ok),Confbridge(${EXTEN},default_bridge,red5sip_user)
-exten => _400X!,n(notavail),Hangup
-]]>
-				</source>
-			</div><br />
-			<div>
-				Modify <tt>/etc/asterisk/confbridge.conf</tt><br />
-				<strong>Add/Modify the following secions</strong>:<br />
-				<source>
-<![CDATA[
-[general]
-
-[red5sip_user]
-type=user
-marked=yes
-dsp_drop_silence=yes
-denoise=true
-
-[sip_user]
-type=user
-end_marked=yes
-wait_marked=yes
-music_on_hold_when_empty=yes
-dsp_drop_silence=yes
-denoise=true
-
-[default_bridge]
-type=bridge
-video_mode=follow_talker
-]]>
-				</source>
-			</div><br />
-			<div>
-				To enable Asterisk Manager API modify <tt>/etc/asterisk/manager.conf</tt><br />
-				<strong>Add/Modify the following sections</strong>:<br />
-				<source>
-<![CDATA[
-[general]
-enabled = yes
-webenabled = no
-port = 5038
-bindaddr = 127.0.0.1
-
-[openmeetings]
-secret = 12345
-deny=0.0.0.0/0.0.0.0
-permit=127.0.0.1/255.255.255.0
-read = all
-write = all
-]]>
-				</source>
-			</div><br />
-			<div>
-				Update OpenMeetings with credentials for Asterisk manager.
-				Modify <tt>/opt/om/webapps/openmeetings/WEB-INF/classes/applicationContext.xml</tt><br />
-				find <strong>&lt;bean id="sipDao" class="org.apache.openmeetings.db.dao.room.SipDao"&gt;</strong>
-				uncomment its parameters and set it to your custom values.<br/>
-				set value for <tt>uid</tt> property to unique secret value (can be generated here <a href="https://www.uuidtools.com">https://www.uuidtools.com</a>)
-				and sync it with <tt>settings.properties</tt> of red5sip (see below)
-				<p style="font-size: larger; color: blue;">
-					IMPORTANT: this step should be done <strong>BEFORE</strong> system install/restore
-					otherwise all SIP related room information will be lost
-				</p>
-			</div><br />
-			<div>
-				Restart asterisk:
-				<source>
-<![CDATA[
-service asterisk restart
-]]>
-				</source>
-			</div><br />
-		</section>
-
-		<section name="Setup red5sip transport">
-			<ul>
-				<li>Download red5sip from <tt>https://github.com/openmeetings/red5sip</tt>
-					<source>
-<![CDATA[
-git clone https://github.com/openmeetings/red5sip.git
-]]>
-					</source>
-				</li>
-				<li>Build with Apache Maven
-					<source>
-<![CDATA[
-cd red5sip
-mvn clean package
-]]>
-					</source>
-				</li>
-				<li>All necessary files will be available in <tt>target</tt> folder, copy/move it to /opt/red5sip/</li>
-				<li>Insert proper values to the <tt>/opt/red5sip/settings.properties</tt>
-					<source>
-<![CDATA[
-red5.host=127.0.0.1 # red5 server address
-om.context=openmeetings # Openmeetings context
-red5.codec=asao
-red5.codec.rate=22 # should correlate with mic setting in Admin->Config `flash.mic.rate`
-sip.obproxy=127.0.0.1 # asterisk adderss
-sip.phone=red5sip_user # sip phone number
-sip.authid=red5sip_user # sip auth id
-sip.secret=12345 # sip password
-sip.realm=asterisk # sip realm
-sip.proxy=127.0.0.1 # address of sip proxy
-rooms.forceStart=no # TBD
-uid=87dddad4-9ca5-475b-860f-2e0825d02b76 #can be generated here: https://www.uuidtools.com/
-rooms=1 # TBD (not in use)
-]]>
-					</source>
-				</li>
-				<li>Set correct permissions on red5sip files:
-					<source>
-<![CDATA[
-sudo chown -R nobody:nogroup /opt/red5sip
-]]>
-					</source>
-				</li>
-				<li>Add red5sip to autostart:
-					<source>
-<![CDATA[
-sudo cp /opt/red5sip/red5sip /etc/init.d/
-sudo chmod a+x /etc/init.d/red5sip
-sudo update-rc.d red5sip defaults
-]]>
-					</source>
-				</li>
-				<li>Start openmeetings
-					<source>
-<![CDATA[
-service red5 start
-]]>
-					</source>
-				</li>
-				<li>
-					Enable <tt>SIP</tt> in openmeetings: <br/>
-					<tt>Administration->Configuration->red5sip.enable == yes</tt>
-				</li>
-				<li>
-					Enable SIP for particular room(s): <br/>
-					<tt>Administration->Conference rooms->Room->Enable SIP transport in the room == checked</tt><br/>
-					(SIP number will be assigned to room if everything is OK)
-				</li>
-				<li>Start red5sip
-					<source>
-<![CDATA[
-service red5sip start
-]]>
-					</source>
-				</li>
-			</ul>
-		</section>
-	</body>
-</document>
diff --git a/openmeetings-service/src/main/java/org/apache/openmeetings/service/notifier/MailNotifier.java b/openmeetings-service/src/main/java/org/apache/openmeetings/service/notifier/MailNotifier.java
index 3be75b2..1aa6bed 100644
--- a/openmeetings-service/src/main/java/org/apache/openmeetings/service/notifier/MailNotifier.java
+++ b/openmeetings-service/src/main/java/org/apache/openmeetings/service/notifier/MailNotifier.java
@@ -26,11 +26,11 @@ import javax.annotation.PostConstruct;
 
 import org.apache.openmeetings.core.notifier.INotifier;
 import org.apache.openmeetings.core.notifier.NotifierService;
-import org.apache.openmeetings.db.dao.room.IInvitationManager;
 import org.apache.openmeetings.db.entity.calendar.Appointment;
 import org.apache.openmeetings.db.entity.room.Invitation;
 import org.apache.openmeetings.db.entity.room.Invitation.MessageType;
 import org.apache.openmeetings.db.entity.user.User;
+import org.apache.openmeetings.db.manager.IInvitationManager;
 import org.apache.openmeetings.service.mail.template.subject.AppointmentReminderTemplate;
 import org.apache.openmeetings.service.mail.template.subject.SubjectEmailTemplate;
 import org.springframework.beans.factory.annotation.Autowired;
diff --git a/openmeetings-service/src/main/java/org/apache/openmeetings/service/room/InvitationManager.java b/openmeetings-service/src/main/java/org/apache/openmeetings/service/room/InvitationManager.java
index 34ae0bc..159c6ea 100644
--- a/openmeetings-service/src/main/java/org/apache/openmeetings/service/room/InvitationManager.java
+++ b/openmeetings-service/src/main/java/org/apache/openmeetings/service/room/InvitationManager.java
@@ -28,7 +28,6 @@ import java.util.TimeZone;
 
 import org.apache.openmeetings.IApplication;
 import org.apache.openmeetings.core.mail.MailHandler;
-import org.apache.openmeetings.db.dao.room.IInvitationManager;
 import org.apache.openmeetings.db.dao.room.InvitationDao;
 import org.apache.openmeetings.db.entity.basic.MailMessage;
 import org.apache.openmeetings.db.entity.calendar.Appointment;
@@ -40,6 +39,7 @@ import org.apache.openmeetings.db.entity.room.Invitation.Valid;
 import org.apache.openmeetings.db.entity.room.Room;
 import org.apache.openmeetings.db.entity.user.User;
 import org.apache.openmeetings.db.entity.user.User.Type;
+import org.apache.openmeetings.db.manager.IInvitationManager;
 import org.apache.openmeetings.service.mail.template.InvitationTemplate;
 import org.apache.openmeetings.service.mail.template.subject.CanceledAppointmentTemplate;
 import org.apache.openmeetings.service.mail.template.subject.CreatedAppointmentTemplate;
diff --git a/openmeetings-service/src/main/java/org/apache/openmeetings/service/quartz/scheduler/AbstractJob.java b/openmeetings-service/src/main/java/org/apache/openmeetings/service/scheduler/AbstractJob.java
similarity index 97%
rename from openmeetings-service/src/main/java/org/apache/openmeetings/service/quartz/scheduler/AbstractJob.java
rename to openmeetings-service/src/main/java/org/apache/openmeetings/service/scheduler/AbstractJob.java
index f1b641b..25db755 100644
--- a/openmeetings-service/src/main/java/org/apache/openmeetings/service/quartz/scheduler/AbstractJob.java
+++ b/openmeetings-service/src/main/java/org/apache/openmeetings/service/scheduler/AbstractJob.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.openmeetings.service.quartz.scheduler;
+package org.apache.openmeetings.service.scheduler;
 
 import static org.apache.openmeetings.util.OpenmeetingsVariables.isInitComplete;
 
diff --git a/openmeetings-service/src/main/java/org/apache/openmeetings/service/quartz/scheduler/AtomReader.java b/openmeetings-service/src/main/java/org/apache/openmeetings/service/scheduler/AtomReader.java
similarity index 98%
rename from openmeetings-service/src/main/java/org/apache/openmeetings/service/quartz/scheduler/AtomReader.java
rename to openmeetings-service/src/main/java/org/apache/openmeetings/service/scheduler/AtomReader.java
index ca3e46e..580b1ab 100644
--- a/openmeetings-service/src/main/java/org/apache/openmeetings/service/quartz/scheduler/AtomReader.java
+++ b/openmeetings-service/src/main/java/org/apache/openmeetings/service/scheduler/AtomReader.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.openmeetings.service.quartz.scheduler;
+package org.apache.openmeetings.service.scheduler;
 
 import static org.apache.openmeetings.core.rss.LoadAtomRssFeed.getFeedConnection;
 
diff --git a/openmeetings-service/src/main/java/org/apache/openmeetings/service/quartz/scheduler/CleanupJob.java b/openmeetings-service/src/main/java/org/apache/openmeetings/service/scheduler/CleanupJob.java
similarity index 90%
rename from openmeetings-service/src/main/java/org/apache/openmeetings/service/quartz/scheduler/CleanupJob.java
rename to openmeetings-service/src/main/java/org/apache/openmeetings/service/scheduler/CleanupJob.java
index 4ced109..5a8ba8e 100644
--- a/openmeetings-service/src/main/java/org/apache/openmeetings/service/quartz/scheduler/CleanupJob.java
+++ b/openmeetings-service/src/main/java/org/apache/openmeetings/service/scheduler/CleanupJob.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.openmeetings.service.quartz.scheduler;
+package org.apache.openmeetings.service.scheduler;
 
 import static org.apache.openmeetings.util.OmFileHelper.EXTENSION_MP4;
 import static org.apache.openmeetings.util.OmFileHelper.TEST_SETUP_PREFIX;
@@ -33,12 +33,19 @@ import org.apache.openmeetings.db.entity.user.User;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
 
+@Component("cleanupJob")
 public class CleanupJob extends AbstractJob {
 	private static Logger log = LoggerFactory.getLogger(CleanupJob.class);
+	@Value("${job.cleanup.session.timeout}")
 	private long sessionTimeout = 30 * 60 * 1000L;
+	@Value("${job.cleanup.test.setup.timeout}")
 	private long testSetupTimeout = 60 * 60 * 1000L; // 1 hour
+	@Value("${job.cleanup.reset.hash.ttl}")
 	private long resetHashTtl = 24 * 60 * 60 * 1000L; // 1 day
+	@Value("${job.cleanup.conf.log.ttl}")
 	private long confLogTtl = 7 * 24 * 60 * 60 * 1000L; // 7 days
 
 	@Autowired
@@ -48,22 +55,6 @@ public class CleanupJob extends AbstractJob {
 	@Autowired
 	private ConferenceLogDao confLogDao;
 
-	public void setSessionTimeout(long sessionTimeout) {
-		this.sessionTimeout = sessionTimeout;
-	}
-
-	public void setTestSetupTimeout(long testSetupTimeout) {
-		this.testSetupTimeout = testSetupTimeout;
-	}
-
-	public void setResetHashTtl(long resetHashTtl) {
-		this.resetHashTtl = resetHashTtl;
-	}
-
-	public void setConfLogTtl(long confLogTtl) {
-		this.confLogTtl = confLogTtl;
-	}
-
 	public void cleanTestSetup() {
 		log.trace("CleanupJob.cleanTestSetup");
 		final long now = System.currentTimeMillis();
diff --git a/openmeetings-service/src/main/java/org/apache/openmeetings/service/quartz/scheduler/ReminderJob.java b/openmeetings-service/src/main/java/org/apache/openmeetings/service/scheduler/ReminderJob.java
similarity index 98%
rename from openmeetings-service/src/main/java/org/apache/openmeetings/service/quartz/scheduler/ReminderJob.java
rename to openmeetings-service/src/main/java/org/apache/openmeetings/service/scheduler/ReminderJob.java
index bd1e32a..9fadd6a 100644
--- a/openmeetings-service/src/main/java/org/apache/openmeetings/service/quartz/scheduler/ReminderJob.java
+++ b/openmeetings-service/src/main/java/org/apache/openmeetings/service/scheduler/ReminderJob.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.openmeetings.service.quartz.scheduler;
+package org.apache.openmeetings.service.scheduler;
 
 import static org.apache.openmeetings.core.rss.LoadAtomRssFeed.setRss;
 import static org.apache.openmeetings.util.OpenmeetingsVariables.CONFIG_DASHBOARD_RSS_FEED1;
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/connection/ConnectionsPanel.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/connection/ConnectionsPanel.java
index a57b704..f2bad46 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/connection/ConnectionsPanel.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/connection/ConnectionsPanel.java
@@ -24,11 +24,10 @@ import static org.apache.openmeetings.web.common.confirmation.ConfirmationBehavi
 
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
-import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import org.apache.openmeetings.core.remote.KurentoHandler;
 import org.apache.openmeetings.core.remote.StreamProcessor;
@@ -76,14 +75,11 @@ public class ConnectionsPanel extends AdminBasePanel {
 			private static final long serialVersionUID = 1L;
 
 			private List<IDataProviderEntity> getConnections() {
-				List<IDataProviderEntity> l = new ArrayList<>();
-				l.addAll(cm.list());
-				Collection<KStreamDto> streams = streamProcessor.getStreams()
+				return Stream.concat(cm.stream()
+						, streamProcessor.getStreams()
 						.stream()
 						.map(KStreamDto::new)
-						.collect(Collectors.toList());
-				l.addAll(streams);
-				return l;
+					).collect(Collectors.toList());
 			}
 
 			@Override
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/rooms/RoomForm.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/rooms/RoomForm.java
index 128f26f..aa73236 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/rooms/RoomForm.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/rooms/RoomForm.java
@@ -434,8 +434,7 @@ public class RoomForm extends AdminBaseForm<Room> {
 
 	void updateClients(AjaxRequestTarget target) {
 		long roomId = getModelObject().getId() != null ? getModelObject().getId() : 0;
-		final List<Client> clientsInRoom = cm.listByRoom(roomId);
-		clients.setDefaultModelObject(clientsInRoom);
+		clients.setDefaultModelObject(cm.streamByRoom(roomId).collect(Collectors.toList()));
 		target.add(clientsContainer);
 	}
 
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application.java
index ac4c389..13e0c97 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application.java
@@ -39,10 +39,12 @@ import java.util.HashSet;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.CompletableFuture;
 
 import javax.websocket.WebSocketContainer;
 
 import org.apache.openmeetings.IApplication;
+import org.apache.openmeetings.core.sip.SipManager;
 import org.apache.openmeetings.core.util.ChatWebSocketHelper;
 import org.apache.openmeetings.core.util.WebSocketHelper;
 import org.apache.openmeetings.db.dao.basic.ConfigurationDao;
@@ -60,6 +62,7 @@ import org.apache.openmeetings.db.entity.room.RoomGroup;
 import org.apache.openmeetings.db.entity.user.GroupUser;
 import org.apache.openmeetings.db.entity.user.User;
 import org.apache.openmeetings.db.entity.user.User.Type;
+import org.apache.openmeetings.db.util.ApplicationHelper;
 import org.apache.openmeetings.db.util.ws.RoomMessage;
 import org.apache.openmeetings.db.util.ws.TextRoomMessage;
 import org.apache.openmeetings.util.OmFileHelper;
@@ -185,6 +188,8 @@ public class Application extends AuthenticatedWebApplication implements IApplica
 	private WhiteboardManager wbManager;
 	@Autowired
 	private AppointmentDao appointmentDao;
+	@Autowired
+	private SipManager sipManager;
 
 	@Override
 	protected void init() {
@@ -348,6 +353,11 @@ public class Application extends AuthenticatedWebApplication implements IApplica
 			recordingDao.resetProcessingStatus(); //we are starting so all processing recordings are now errors
 			userManager.initHttpClient();
 			setInitComplete(true);
+			CompletableFuture.runAsync(() -> {
+				ThreadContext.setApplication(Application.this);
+				ApplicationHelper.ensureRequestCycle(Application.this);
+				sipManager.setUserPicture(u -> ProfileImageResourceReference.getUrl(RequestCycle.get(), u));
+			});
 		} catch (Exception err) {
 			log.error("[appStart]", err);
 		}
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/ClientManager.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/ClientManager.java
index 317f9ee..4dfedcc 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/ClientManager.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/ClientManager.java
@@ -23,21 +23,21 @@ import static org.apache.openmeetings.web.app.WebSession.getUserId;
 import static org.apache.openmeetings.web.pages.auth.SignInPage.TOKEN_PARAM;
 
 import java.io.Serializable;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Function;
-import java.util.function.Predicate;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import org.apache.openmeetings.core.remote.KurentoHandler;
+import org.apache.openmeetings.core.sip.SipManager;
 import org.apache.openmeetings.db.dao.log.ConferenceLogDao;
 import org.apache.openmeetings.db.entity.basic.Client;
 import org.apache.openmeetings.db.entity.log.ConferenceLog;
@@ -78,6 +78,10 @@ public class ClientManager implements IClientManager {
 	private Application app;
 	@Autowired
 	private KurentoHandler kHandler;
+	@Autowired
+	private SipManager sipManager;
+	@Autowired
+	private TimerService timerService;
 
 	private IMap<String, Client> map() {
 		return app.hazelcast.getMap(ONLINE_USERS_KEY);
@@ -170,14 +174,12 @@ public class ClientManager implements IClientManager {
 		if (roomId != null) {
 			IMap<Long, Set<String>> rooms = rooms();
 			rooms.lock(roomId);
-			Set<String> clients = rooms.get(roomId);
-			if (clients != null) {
-				clients.remove(c.getUid());
-				rooms.put(roomId, clients);
-				onlineRooms.put(roomId, clients);
-			}
+			Set<String> clients = rooms.getOrDefault(roomId, ConcurrentHashMap.newKeySet());
+			clients.remove(c.getUid());
+			rooms.put(roomId, clients);
+			onlineRooms.put(roomId, clients);
 			rooms.unlock(roomId);
-			if (clients == null || clients.isEmpty()) {
+			if (clients.isEmpty()) {
 				String serverId = c.getServerId();
 				IMap<String, ServerInfo> servers = servers();
 				servers.lock(serverId);
@@ -258,8 +260,7 @@ public class ClientManager implements IClientManager {
 		log.debug("Adding online room client: {}, room: {}", c.getUid(), roomId);
 		IMap<Long, Set<String>> rooms = rooms();
 		rooms.lock(roomId);
-		rooms.putIfAbsent(roomId, ConcurrentHashMap.newKeySet());
-		Set<String> set = rooms.get(roomId);
+		Set<String> set = rooms.getOrDefault(roomId, ConcurrentHashMap.newKeySet());
 		set.add(c.getUid());
 		final int count = set.size();
 		rooms.put(roomId, set);
@@ -268,6 +269,7 @@ public class ClientManager implements IClientManager {
 		String serverId = c.getServerId();
 		addRoomToServer(serverId, r);
 		update(c);
+		timerService.scheduleSipCheck(r);
 		return count;
 	}
 
@@ -296,8 +298,8 @@ public class ClientManager implements IClientManager {
 	}
 
 	@Override
-	public List<Client> list() {
-		return new ArrayList<>(map().values());
+	public Stream<Client> stream() {
+		return map().values().stream();
 	}
 
 	@Override
@@ -306,50 +308,24 @@ public class ClientManager implements IClientManager {
 	}
 
 	@Override
-	public List<Client> listByRoom(Long roomId) {
-		return listByRoom(roomId, null);
-	}
-
-	public List<Client> listByRoom(Long roomId, Predicate<Client> filter) {
-		List<Client> clients = new ArrayList<>();
-		if (roomId != null) {
-			Set<String> uids = onlineRooms.get(roomId);
-			if (uids != null) {
-				for (String uid : uids) {
-					Client c = get(uid);
-					if (c != null && (filter == null || filter.test(c))) {
-						clients.add(c);
-					}
-				}
-			}
-		}
-		return clients;
-	}
-
-	public Set<Long> listRoomIds(Long userId) {
-		Set<Long> result = new HashSet<>();
-		for (Entry<Long, Set<String>> me : onlineRooms.entrySet()) {
-			for (String uid : me.getValue()) {
-				Client c = get(uid);
-				if (c != null && c.sameUserId(userId)) {
-					result.add(me.getKey());
-				}
-			}
-		}
-		return result;
+	public Stream<Client> streamByRoom(Long roomId) {
+		return Optional.ofNullable(roomId)
+			.map(id -> onlineRooms.getOrDefault(id, Set.of()))
+			.stream()
+			.flatMap(Set::stream)
+			.map(uid -> get(uid))
+			.filter(Objects::nonNull);
 	}
 
 	public boolean isInRoom(long roomId, long userId) {
-		Set<String> clients = onlineRooms.get(roomId);
-		if (clients != null) {
-			for (String uid : clients) {
-				Client c = get(uid);
-				if (c != null && c.sameUserId(userId)) {
-					return true;
-				}
-			}
-		}
-		return false;
+		return Optional.of(roomId)
+			.map(id -> onlineRooms.getOrDefault(id, Set.of()))
+			.stream()
+			.flatMap(Set::stream)
+			.map(uid -> get(uid))
+			.filter(c -> c != null && c.sameUserId(userId))
+			.findAny()
+			.isPresent();
 	}
 
 	private List<Client> getByKeys(Long userId, String sessionId) {
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/TimerService.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/TimerService.java
index 906426f..799b5a1 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/TimerService.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/TimerService.java
@@ -21,13 +21,17 @@ package org.apache.openmeetings.web.app;
 import static java.util.concurrent.CompletableFuture.delayedExecutor;
 
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
 
 import javax.annotation.PostConstruct;
 
+import org.apache.openmeetings.core.sip.SipManager;
 import org.apache.openmeetings.core.util.WebSocketHelper;
+import org.apache.openmeetings.db.entity.basic.Client;
 import org.apache.openmeetings.db.entity.room.Room;
 import org.apache.openmeetings.db.entity.room.Room.Right;
 import org.apache.openmeetings.db.entity.user.User;
@@ -42,15 +46,19 @@ import org.springframework.stereotype.Service;
 public class TimerService {
 	private static final Logger log = LoggerFactory.getLogger(TimerService.class);
 	private int modCheckInterval = 5;
+	private int sipCheckInterval = 2;
 	private final User sysUser = new User();
 	private final Map<Long, CompletableFuture<Object>> modCheckMap = new ConcurrentHashMap<>();
+	private final Map<Long, CompletableFuture<Object>> sipCheckMap = new ConcurrentHashMap<>();
 
 	@Autowired
 	private ClientManager cm;
+	@Autowired
+	private SipManager sipManager;
 
 	@PostConstruct
 	private void init() {
-		sysUser.setId(-1L);
+		sysUser.setId(-5L);
 		sysUser.setDisplayName("System");
 	}
 
@@ -59,19 +67,64 @@ public class TimerService {
 				roomId
 				, new CompletableFuture<>().completeAsync(() -> {
 					log.warn("Moderator room check {}", roomId);
-					if (cm.listByRoom(roomId).isEmpty()) {
+					if (cm.streamByRoom(roomId).findAny().isEmpty()) {
 						modCheckMap.remove(roomId);
 					} else {
-						WebSocketHelper.sendRoom(new TextRoomMessage(roomId, sysUser, RoomMessage.Type.MODERATOR_IN_ROOM, "" + !cm.listByRoom(roomId, c -> c.hasRight(Right.MODERATOR)).isEmpty()));
+						WebSocketHelper.sendRoom(new TextRoomMessage(roomId, sysUser, RoomMessage.Type.MODERATOR_IN_ROOM
+								, "" + !cm.streamByRoom(roomId).filter(c -> c.hasRight(Right.MODERATOR)).findAny().isEmpty()));
 						doModCheck(roomId);
 					}
 					return null;
 				}, delayedExecutor(modCheckInterval, TimeUnit.SECONDS)));
 	}
 
+	private void doSipCheck(Long roomId) {
+		sipCheckMap.put(
+				roomId
+				, new CompletableFuture<>().completeAsync(() -> {
+					log.warn("Sip room check {}", roomId);
+					Optional<Client> sipClient = cm.streamByRoom(roomId).filter(Client::isSip).findAny();
+					cm.streamByRoom(roomId).filter(Predicate.not(Client::isSip)).findAny().ifPresentOrElse(c -> {
+						updateSipLastName(sipClient, c.getRoom());
+						doSipCheck(roomId);
+					}, () -> {
+						log.warn("No more clients in the room {}", roomId);
+						sipCheckMap.remove(roomId);
+						sipClient.ifPresent(cm::exit);
+					});
+					return null;
+				}, delayedExecutor(sipCheckInterval, TimeUnit.SECONDS)));
+	}
+
+	private void updateSipLastName(Optional<Client> sipClient, Room r) {
+		final String newLastName = "(" + sipManager.countUsers(r.getConfno()) + ")";
+		sipClient.ifPresentOrElse(c -> {
+			if (!newLastName.equals(c.getUser().getLastname())) {
+				c.getUser().setLastname(newLastName).resetDisplayName();
+				cm.update(c);
+				WebSocketHelper.sendRoom(new TextRoomMessage(r.getId(), c, RoomMessage.Type.RIGHT_UPDATED, c.getUid()));
+			}
+		}, () -> {
+			User sipUser = sipManager.getSipUser(r);
+			sipUser.setLastname(newLastName).resetDisplayName();
+			Client c = new Client("-- unique - sip - session --", 1, sipUser, sipUser.getPictureUri());
+			cm.add(c);
+			c.setRoom(r);
+			cm.addToRoom(c);
+			WebSocketHelper.sendRoom(new TextRoomMessage(r.getId(), c, RoomMessage.Type.ROOM_ENTER, c.getUid()));
+		});
+	}
+
 	public void scheduleModCheck(Room r) {
-		if (r.isModerated() && r.isWaitModerator() && !cm.listByRoom(r.getId()).isEmpty() && !modCheckMap.containsKey(r.getId())) {
+		if (r.isModerated() && r.isWaitModerator() && !modCheckMap.containsKey(r.getId())) {
 			doModCheck(r.getId());
 		}
 	}
+
+	public void scheduleSipCheck(Room r) {
+		// sip allowed and configured
+		if (sipManager.getSipUser(r) != null && !sipCheckMap.containsKey(r.getId())) {
+			doSipCheck(r.getId());
+		}
+	}
 }
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/UserManager.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/UserManager.java
index 53265d5..76a8dcf 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/UserManager.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/UserManager.java
@@ -223,9 +223,7 @@ public class UserManager implements IUserManager {
 	@Override
 	public boolean kickUsersByRoomId(Long roomId) {
 		try {
-			for (Client c : cm.listByRoom(roomId)) {
-				Application.kickUser(c);
-			}
+			cm.streamByRoom(roomId).forEach(Application::kickUser);
 			return true;
 		} catch (Exception err) {
 			log.error("[kickUsersByRoomId]", err);
@@ -235,7 +233,7 @@ public class UserManager implements IUserManager {
 
 	@Override
 	public boolean kickExternal(Long roomId, String externalType, String externalId) {
-		boolean result = false;
+		Boolean result = false;
 		try {
 			if (roomId == null) {
 				return result;
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/RoomPanel.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/RoomPanel.java
index 73f9049..4a4ae17 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/RoomPanel.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/RoomPanel.java
@@ -44,7 +44,6 @@ import org.apache.openmeetings.db.dao.calendar.AppointmentDao;
 import org.apache.openmeetings.db.dao.file.FileItemDao;
 import org.apache.openmeetings.db.dao.user.UserDao;
 import org.apache.openmeetings.db.entity.basic.Client;
-import org.apache.openmeetings.db.entity.basic.Client.StreamDesc;
 import org.apache.openmeetings.db.entity.calendar.Appointment;
 import org.apache.openmeetings.db.entity.calendar.MeetingMember;
 import org.apache.openmeetings.db.entity.file.BaseFileItem;
@@ -167,9 +166,9 @@ public class RoomPanel extends BasePanel {
 				sidebar.setFilesActive(target);
 			}
 			if (Room.Type.PRESENTATION != r.getType()) {
-				List<Client> mods = cm.listByRoom(r.getId(), cl -> cl.hasRight(Room.Right.MODERATOR));
-				log.debug("RoomPanel::roomEnter, mods IS EMPTY ? {}, is MOD ? {}", mods.isEmpty(), c.hasRight(Room.Right.MODERATOR));
-				if (mods.isEmpty()) {
+				boolean modsEmpty = noModerators();
+				log.debug("RoomPanel::roomEnter, mods IS EMPTY ? {}, is MOD ? {}", modsEmpty, c.hasRight(Room.Right.MODERATOR));
+				if (modsEmpty) {
 					showIdeaAlert(target, getString(r.isModerated() ? "641" : "498"));
 				}
 			}
@@ -182,11 +181,10 @@ public class RoomPanel extends BasePanel {
 		private void initVideos(AjaxRequestTarget target) {
 			StringBuilder sb = new StringBuilder();
 			JSONArray streams = new JSONArray();
-			for (Client c : cm.listByRoom(getRoom().getId())) {
-				for (StreamDesc sd : c.getStreams()) {
-					streams.put(sd.toJson());
-				}
-			}
+			cm.streamByRoom(getRoom().getId())
+				.map(Client::getStreams)
+				.flatMap(List::stream)
+				.forEach(sd -> streams.put(sd.toJson()));
 			if (streams.length() > 0) {
 				sb.append("VideoManager.play(").append(streams).append(", ").append(kHandler.getTurnServers(getClient())).append(");");
 			}
@@ -312,7 +310,7 @@ public class RoomPanel extends BasePanel {
 		add(roomClosed = new RedirectMessageDialog("room-closed", "1098", r.isClosed(), r.getRedirectURL()));
 		if (r.isClosed()) {
 			room.setVisible(false);
-		} else if (cm.listByRoom(r.getId()).size() >= r.getCapacity()) {
+		} else if (cm.streamByRoom(r.getId()).count() >= r.getCapacity()) {
 			accessDenied = new ExpiredMessageDialog(ACCESS_DENIED_ID, getString("99"), menu);
 			room.setVisible(false);
 		} else if (r.getId().equals(WebSession.get().getRoomId())) {
@@ -397,7 +395,7 @@ public class RoomPanel extends BasePanel {
 			}
 			if (r.isModerated() && r.isWaitModerator()
 					&& !c.hasRight(Right.MODERATOR)
-					&& cm.listByRoom(r.getId(), cl -> cl.hasRight(Right.MODERATOR)).isEmpty())
+					&& noModerators())
 			{
 				room.setVisible(false);
 				createWaitModerator(true);
@@ -620,13 +618,9 @@ public class RoomPanel extends BasePanel {
 			if (streamProcessor.isRecording(r.getId())) {
 				handler.appendJavaScript("if (typeof(WbArea) === 'object') {WbArea.setRecStarted(true);}");
 			} else if (streamProcessor.recordingAllowed(getClient())) {
-				boolean hasStreams = false;
-				for (Client cl : cm.listByRoom(r.getId())) {
-					if (!cl.getStreams().isEmpty()) {
-						hasStreams = true;
-						break;
-					}
-				}
+				boolean hasStreams = cm.streamByRoom(r.getId())
+						.filter(cl -> !cl.getStreams().isEmpty())
+						.findAny().isPresent();
 				handler.appendJavaScript(String.format("if (typeof(WbArea) === 'object') {WbArea.setRecStarted(false);WbArea.setRecEnabled(%s);}", hasStreams));
 			}
 		}
@@ -641,12 +635,9 @@ public class RoomPanel extends BasePanel {
 	}
 
 	public static boolean hasRight(ClientManager cm, long userId, long roomId, Right r) {
-		for (Client c : cm.listByRoom(roomId)) {
-			if (c.sameUserId(userId) && c.hasRight(r)) {
-				return true;
-			}
-		}
-		return false;
+		return cm.streamByRoom(roomId)
+				.filter(c -> c.sameUserId(userId) && c.hasRight(r))
+				.findAny().isPresent();
 	}
 
 	@Override
@@ -699,8 +690,7 @@ public class RoomPanel extends BasePanel {
 
 	public void requestRight(Right right, IPartialPageRequestHandler handler) {
 		RoomMessage.Type reqType = null;
-		List<Client> mods = cm.listByRoom(r.getId(), c -> c.hasRight(Room.Right.MODERATOR));
-		if (mods.isEmpty()) {
+		if (noModerators()) {
 			if (r.isModerated()) {
 				showIdeaAlert(handler, getString("696"));
 				return;
@@ -881,7 +871,7 @@ public class RoomPanel extends BasePanel {
 
 	private CharSequence createAddClientJs(Client c) {
 		JSONArray arr = new JSONArray();
-		cm.listByRoom(r.getId()).stream().forEach(cl -> arr.put(cl.toJson(c.getUid().equals(cl.getUid()))));
+		cm.streamByRoom(r.getId()).forEach(cl -> arr.put(cl.toJson(c.getUid().equals(cl.getUid()))));
 		return new StringBuilder()
 				.append("Room.addClient(")
 				.append(arr.toString(new NullStringer()))
@@ -909,4 +899,11 @@ public class RoomPanel extends BasePanel {
 		}
 		return res;
 	}
+
+	private boolean noModerators() {
+		return cm.streamByRoom(r.getId())
+				.filter(cl -> cl.hasRight(Room.Right.MODERATOR))
+				.findAny()
+				.isEmpty();
+	}
 }
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/menu/SipDialerDialog.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/menu/SipDialerDialog.java
index eb31b18..e54a13a 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/menu/SipDialerDialog.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/menu/SipDialerDialog.java
@@ -18,7 +18,7 @@
  */
 package org.apache.openmeetings.web.room.menu;
 
-import org.apache.openmeetings.db.dao.room.SipDao;
+import org.apache.openmeetings.core.sip.SipManager;
 import org.apache.openmeetings.web.common.OmModalCloseButton;
 import org.apache.openmeetings.web.room.RoomPanel;
 import org.apache.wicket.ajax.AjaxRequestTarget;
@@ -41,7 +41,7 @@ public class SipDialerDialog extends Modal<String> {
 	private final TextField<String> number = new TextField<>("number", Model.of(""));
 	private final RoomPanel room;
 	@SpringBean
-	private SipDao sipDao;
+	private SipManager sipDao;
 
 	public SipDialerDialog(String id, RoomPanel room) {
 		super(id);
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/wb/WbWebSocketHelper.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/wb/WbWebSocketHelper.java
index c7b8232..df58970 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/wb/WbWebSocketHelper.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/wb/WbWebSocketHelper.java
@@ -18,6 +18,7 @@
  */
 package org.apache.openmeetings.web.room.wb;
 
+import static org.apache.openmeetings.core.util.WebSocketHelper.alwaysTrue;
 import static org.apache.openmeetings.core.util.WebSocketHelper.publish;
 import static org.apache.openmeetings.core.util.WebSocketHelper.sendRoom;
 import static org.apache.openmeetings.util.OpenmeetingsVariables.PARAM_SRC;
@@ -74,7 +75,7 @@ public class WbWebSocketHelper {
 		if (publish) {
 			publish(new WsMessageWb(roomId, meth, obj, null));
 		}
-		sendWb(roomId, meth, obj, null);
+		sendWb(roomId, meth, obj, alwaysTrue());
 	}
 
 	public static void sendWbOthers(Long roomId, WbAction meth, JSONObject obj, final String uid) {
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/MessageDialog.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/MessageDialog.java
index 9aca029..9791449 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/MessageDialog.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/MessageDialog.java
@@ -38,7 +38,6 @@ import java.util.List;
 import org.apache.openmeetings.core.mail.MailHandler;
 import org.apache.openmeetings.db.dao.basic.ConfigurationDao;
 import org.apache.openmeetings.db.dao.calendar.AppointmentDao;
-import org.apache.openmeetings.db.dao.room.IInvitationManager;
 import org.apache.openmeetings.db.dao.room.RoomDao;
 import org.apache.openmeetings.db.dao.user.PrivateMessageDao;
 import org.apache.openmeetings.db.dao.user.UserDao;
@@ -50,6 +49,7 @@ import org.apache.openmeetings.db.entity.room.Room;
 import org.apache.openmeetings.db.entity.user.PrivateMessage;
 import org.apache.openmeetings.db.entity.user.User;
 import org.apache.openmeetings.db.entity.user.User.Type;
+import org.apache.openmeetings.db.manager.IInvitationManager;
 import org.apache.openmeetings.util.CalendarHelper;
 import org.apache.openmeetings.web.app.Application;
 import org.apache.openmeetings.web.app.WebSession;
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/rooms/RoomListPanel.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/rooms/RoomListPanel.java
index cd4dfd4..3576fc8 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/rooms/RoomListPanel.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/rooms/RoomListPanel.java
@@ -70,7 +70,7 @@ public class RoomListPanel extends Panel {
 			final WebMarkupContainer info = new WebMarkupContainer("info");
 			roomContainer.add(info.setOutputMarkupId(true)
 					.add(AttributeModifier.append(ATTR_TITLE, getString(String.format("room.type.%s.desc", r.getType().name())))));
-			final Label curUsers = new Label("curUsers", new Model<>(cm.listByRoom(r.getId()).size()));
+			final Label curUsers = new Label("curUsers", new Model<>(cm.streamByRoom(r.getId()).count()));
 			roomContainer.add(curUsers.setOutputMarkupId(true));
 			roomContainer.add(new Label("totalUsers", r.getCapacity()));
 			item.add(new WebMarkupContainer("btn").add(new Label("label", label)).add(new RoomEnterBehavior(r.getId()) {
@@ -95,7 +95,7 @@ public class RoomListPanel extends Panel {
 
 				@Override
 				public void onClick(AjaxRequestTarget target) {
-					target.add(curUsers.setDefaultModelObject(cm.listByRoom(r.getId()).size()));
+					target.add(curUsers.setDefaultModelObject(cm.streamByRoom(r.getId()).count()));
 					onRefreshClick(target, r);
 				}
 			}.add(AttributeModifier.append(ATTR_TITLE, new ResourceModel("lbl.refresh"))));
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/rooms/RoomsPanel.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/rooms/RoomsPanel.java
index f110da7..4760969 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/rooms/RoomsPanel.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/rooms/RoomsPanel.java
@@ -22,6 +22,7 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.InputStream;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.apache.openmeetings.db.dao.room.RoomDao;
 import org.apache.openmeetings.db.dao.user.UserDao;
@@ -128,7 +129,7 @@ public class RoomsPanel extends UserPanel {
 	}
 
 	void updateRoomDetails(AjaxRequestTarget target) {
-		clients.setDefaultModelObject(cm.listByRoom(roomId));
+		clients.setDefaultModelObject(cm.streamByRoom(roomId).collect(Collectors.toList()));
 		Room room = roomDao.get(roomId);
 		roomIdLbl.setDefaultModelObject(room.getId());
 		roomNameLbl.setDefaultModelObject(room.getName());
diff --git a/openmeetings-web/src/main/webapp/WEB-INF/classes/applicationContext.xml b/openmeetings-web/src/main/webapp/WEB-INF/classes/applicationContext.xml
index 0edbe2b..e8391ad 100644
--- a/openmeetings-web/src/main/webapp/WEB-INF/classes/applicationContext.xml
+++ b/openmeetings-web/src/main/webapp/WEB-INF/classes/applicationContext.xml
@@ -40,18 +40,9 @@
 	<context:annotation-config />
 	<context:component-scan base-package="org.apache.openmeetings" />
 
-	<!--
-			5000		== 5 sec
-			300000		== 5 min
-			900000		== 15 min
-			1800000		== 30 min
-			3600000		== 1 hour
-			86400000	== 1 day
-			604800000	== 7 days
-	 -->
-	<bean id="cleanupJob" class="org.apache.openmeetings.service.quartz.scheduler.CleanupJob"
-			p:sessionTimeout="1800000" p:testSetupTimeout="3600000" p:resetHashTtl="86400000"
-			p:confLogTtl="604800000" />
+	<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"
+		p:location="classpath:openmeetings.properties" />
+
 	<!-- sessions clean-up -->
 	<bean id="cleanSessionsJobDetails" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"
 			p:targetObject-ref="cleanupJob" p:targetMethod="cleanSessions" p:concurrent="false" />
@@ -124,46 +115,10 @@
 	</bean>
 	<!-- End of Services -->
 
-	<bean class="org.apache.openmeetings.db.dao.room.SipConfig">
-		<!-- Should be uncommented and updated with real values for Asterisk-
-		<property name="sipHostname" value="192.168.1.102"/>
-		<property name="managerPort" value="5038"/>
-		<property name="managerUser" value="openmeetings"/>
-		<property name="managerPass" value="12345"/>
-		<property name="managerTimeout" value="10000"/>
-
-		<property name="localWsPort" value="6666"/>
-		<property name="localWsHost" value="192.168.1.211"/>
-		<property name="wsPort" value="8088"/>
-		<property name="omSipUser" value="omsip_user"/>
-		<property name="omSipPasswd" value="12345"/>
-
-		<property name="uid" value="87dddad4-9ca5-475b-860f-2e0825d02b76"/>
-		-  -->
-	</bean>
-
 	<!-- Thread Executor -->
 	<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
 		<property name="corePoolSize" value="5" />
 		<property name="maxPoolSize" value="10" />
 		<property name="queueCapacity" value="25" />
 	</bean>
-
-	<!-- Kurento -->
-	<!-- please ensure `p:kuid` below is unique, better to regenerate it from time to time -->
-	<!-- `p:ignoredKuids` can be space and/or comma separated -->
-	<bean id="kurentoHandler" class="org.apache.openmeetings.core.remote.KurentoHandler"
-			p:kurentoWsUrl="ws://127.0.0.1:8888/kurento"
-			p:checkTimeout="10000"
-			p:watchThreadCount="10"
-			p:turnUrl=""
-			p:turnUser=""
-			p:turnSecret=""
-			p:turnMode="rest"
-			p:turnTtl="60"
-			p:objCheckTimeout="200"
-			p:flowoutTimeout="5"
-			p:kuid="df992960-e7b0-11ea-9acd-337fb30dd93d"
-			p:ignoredKuids=""
-			/>
 </beans>
diff --git a/openmeetings-web/src/main/webapp/WEB-INF/classes/openmeetings.properties b/openmeetings-web/src/main/webapp/WEB-INF/classes/openmeetings.properties
new file mode 100644
index 0000000..5f566d8
--- /dev/null
+++ b/openmeetings-web/src/main/webapp/WEB-INF/classes/openmeetings.properties
@@ -0,0 +1,65 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+#
+################## Timeouts ##################
+#			5000		== 5 sec
+#			300000		== 5 min
+#			900000		== 15 min
+#			1800000		== 30 min
+#			3600000		== 1 hour
+#			86400000	== 1 day
+#			604800000	== 7 days
+job.cleanup.session.timeout=1800000
+job.cleanup.test.setup.timeout=3600000
+job.cleanup.reset.hash.ttl=86400000
+job.cleanup.conf.log.ttl=604800000
+
+################## Kurento ##################
+kurento.ws.url=ws://127.0.0.1:8888/kurento
+kurento.turn.url=
+kurento.turn.user=
+kurento.turn.secret=
+kurento.turn.mode=rest
+## minutes
+kurento.turn.ttl=60
+## milliseconds
+kurento.check.timeout=10000
+## milliseconds
+kurento.object.check.timeout=200
+kurento.watch.thread.count=10
+kurento.flowout.timeout=5
+## please ensure this one is unique, better to regenerate it from time to time
+## can be generated for ex. here https://www.uuidtools.com
+kurento.kuid=df992960-e7b0-11ea-9acd-337fb30dd93d
+## this list can be space and/or comma separated
+kurento.ignored.kuids=
+
+################## SIP ##################
+### Should be uncommented and updated with real values for Asterisk ###
+#sip.hostname=192.168.1.102
+#sip.manager.port=5038
+#sip.manager.user=openmeetings
+#sip.manager.password=12345
+#sip.manager.timeout=10000
+
+#sip.ws.local.port=6666
+## 127.0.0.1 is NOT working here
+#sip.ws.local.host=192.168.1.211
+#sip.ws.remote.port=8088
+#sip.ws.remote.user=omsip_user
+#sip.ws.remote.password=12345
diff --git a/openmeetings-web/src/test/java/org/apache/openmeetings/service/quartz/TestJob.java b/openmeetings-web/src/test/java/org/apache/openmeetings/service/quartz/TestJob.java
index 01be361..58edf05 100644
--- a/openmeetings-web/src/test/java/org/apache/openmeetings/service/quartz/TestJob.java
+++ b/openmeetings-web/src/test/java/org/apache/openmeetings/service/quartz/TestJob.java
@@ -23,8 +23,8 @@ import static org.apache.openmeetings.util.OpenmeetingsVariables.setInitComplete
 
 import org.apache.openmeetings.AbstractWicketTester;
 import org.apache.openmeetings.db.entity.basic.Configuration;
-import org.apache.openmeetings.service.quartz.scheduler.CleanupJob;
-import org.apache.openmeetings.service.quartz.scheduler.ReminderJob;
+import org.apache.openmeetings.service.scheduler.CleanupJob;
+import org.apache.openmeetings.service.scheduler.ReminderJob;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 
diff --git a/openmeetings-webservice/src/main/java/org/apache/openmeetings/webservice/RoomWebService.java b/openmeetings-webservice/src/main/java/org/apache/openmeetings/webservice/RoomWebService.java
index dd4a83c..d704577 100644
--- a/openmeetings-webservice/src/main/java/org/apache/openmeetings/webservice/RoomWebService.java
+++ b/openmeetings-webservice/src/main/java/org/apache/openmeetings/webservice/RoomWebService.java
@@ -22,6 +22,7 @@ import static org.apache.openmeetings.webservice.Constants.TNS;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import javax.jws.WebMethod;
 import javax.jws.WebParam;
@@ -46,7 +47,6 @@ import org.apache.openmeetings.db.dto.basic.ServiceResult.Type;
 import org.apache.openmeetings.db.dto.room.InvitationDTO;
 import org.apache.openmeetings.db.dto.room.RoomDTO;
 import org.apache.openmeetings.db.dto.user.UserDTO;
-import org.apache.openmeetings.db.entity.basic.Client;
 import org.apache.openmeetings.db.entity.room.Invitation;
 import org.apache.openmeetings.db.entity.room.Invitation.MessageType;
 import org.apache.openmeetings.db.entity.room.Room;
@@ -349,7 +349,7 @@ public class RoomWebService extends BaseWebService {
 	@GET
 	@Path("/count/{roomid}")
 	public ServiceResult count(@WebParam(name="sid") @QueryParam("sid") String sid, @WebParam(name="roomid") @PathParam("roomid") Long roomId) {
-		return performCall(sid, User.Right.SOAP, sd -> new ServiceResult(String.valueOf(clientManager.listByRoom(roomId).size()), Type.SUCCESS));
+		return performCall(sid, User.Right.SOAP, sd -> new ServiceResult(String.valueOf(clientManager.streamByRoom(roomId).count()), Type.SUCCESS));
 	}
 
 	/**
@@ -364,11 +364,9 @@ public class RoomWebService extends BaseWebService {
 	@Path("/users/{roomid}")
 	public List<UserDTO> users(@WebParam(name="sid") @QueryParam("sid") String sid, @WebParam(name="roomid") @PathParam("roomid") Long roomId) {
 		return performCall(sid, User.Right.SOAP, sd -> {
-			List<UserDTO> result = new ArrayList<>();
-			for (Client c : clientManager.listByRoom(roomId)) {
-				result.add(new UserDTO(c.getUser()));
-			}
-			return result;
+			return clientManager.streamByRoom(roomId)
+					.map(c -> new UserDTO(c.getUser()))
+					.collect(Collectors.toList());
 		});
 	}
 
diff --git a/pom.xml b/pom.xml
index 60b4cf4..959ee7e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -616,18 +616,6 @@
 				</exclusions>
 			</dependency>
 			<dependency>
-				<groupId>org.mockito</groupId>
-				<artifactId>mockito-inline</artifactId>
-				<version>${mockito.version}</version>
-				<scope>test</scope>
-			</dependency>
-			<dependency>
-				<groupId>org.mockito</groupId>
-				<artifactId>mockito-junit-jupiter</artifactId>
-				<version>${mockito.version}</version>
-				<scope>test</scope>
-			</dependency>
-			<dependency>
 				<groupId>javax.websocket</groupId>
 				<artifactId>javax.websocket-api</artifactId>
 				<version>1.1</version>
@@ -644,16 +632,33 @@
 				<version>4.3.0</version>
 			</dependency>
 			<dependency>
-				<groupId>org.junit.jupiter</groupId>
-				<artifactId>junit-jupiter-params</artifactId>
-				<version>${junit.version}</version>
-				<scope>test</scope>
+				<groupId>org.asteriskjava</groupId>
+				<artifactId>asterisk-java</artifactId>
+				<version>${asterisk-java.version}</version>
 			</dependency>
 			<dependency>
 				<groupId>javax.sip</groupId>
 				<artifactId>jain-sip-ri</artifactId>
 				<version>${jain-sip.version}</version>
 			</dependency>
+			<dependency>
+				<groupId>org.mockito</groupId>
+				<artifactId>mockito-inline</artifactId>
+				<version>${mockito.version}</version>
+				<scope>test</scope>
+			</dependency>
+			<dependency>
+				<groupId>org.mockito</groupId>
+				<artifactId>mockito-junit-jupiter</artifactId>
+				<version>${mockito.version}</version>
+				<scope>test</scope>
+			</dependency>
+			<dependency>
+				<groupId>org.junit.jupiter</groupId>
+				<artifactId>junit-jupiter-params</artifactId>
+				<version>${junit.version}</version>
+				<scope>test</scope>
+			</dependency>
 		</dependencies>
 	</dependencyManagement>
 	<dependencies>