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/12/22 14:41:06 UTC

[openmeetings] branch master updated: [OPENMEETINGS-2239] another attempt to make 'softphone -> browser' work

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 5608cbf  [OPENMEETINGS-2239] another attempt to make 'softphone -> browser' work
5608cbf is described below

commit 5608cbfee7be52e625f0197f1dbad0d53f1421e9
Author: Maxim Solodovnik <so...@gmail.com>
AuthorDate: Tue Dec 22 21:40:51 2020 +0700

    [OPENMEETINGS-2239] another attempt to make 'softphone -> browser' work
---
 .../openmeetings/core/remote/AbstractStream.java   |  12 +-
 .../org/apache/openmeetings/core/remote/KRoom.java |  26 ++--
 .../apache/openmeetings/core/remote/KStream.java   | 140 ++++++++++++---------
 .../openmeetings/core/remote/KTestStream.java      |   4 +-
 .../openmeetings/core/sip/ISipCallbacks.java       |   4 +-
 .../apache/openmeetings/core/sip/SipManager.java   |  47 ++++---
 .../openmeetings/core/sip/SipStackProcessor.java   |  18 +--
 .../openmeetings/core/remote/BaseMockedTest.java   |   3 +-
 .../src/site/markdown/AsteriskIntegration.md       |   1 -
 .../web/admin/connection/KStreamDto.java           |  40 +++---
 .../apache/openmeetings/web/app/TimerService.java  |   3 +-
 .../org/apache/openmeetings/web/room/raw-room.js   |  28 +++--
 12 files changed, 181 insertions(+), 145 deletions(-)

diff --git a/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/AbstractStream.java b/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/AbstractStream.java
index cfc18d4..2dd7230 100644
--- a/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/AbstractStream.java
+++ b/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/AbstractStream.java
@@ -48,8 +48,16 @@ public abstract class AbstractStream {
 
 	public abstract void release(boolean remove);
 
-	public static WebRtcEndpoint createWebRtcEndpoint(MediaPipeline pipeline) {
-		return new WebRtcEndpoint.Builder(pipeline).build();
+	public static WebRtcEndpoint createWebRtcEndpoint(MediaPipeline pipeline, Boolean send) {
+		WebRtcEndpoint.Builder builder = new WebRtcEndpoint.Builder(pipeline);
+		if (send != null) {
+			if (send) {
+				builder.sendonly();
+			} else {
+				builder.recvonly();
+			}
+		}
+		return builder.build();
 	}
 
 	public static RecorderEndpoint createRecorderEndpoint(MediaPipeline pipeline, String path, MediaProfileSpecType profile) {
diff --git a/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KRoom.java b/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KRoom.java
index 2062c5b..bc90f9b 100644
--- a/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KRoom.java
+++ b/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KRoom.java
@@ -1,7 +1,4 @@
 /*
- * (C) Copyright 2014 Kurento (http://kurento.org/)
- */
-/*
  * 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
@@ -52,15 +49,12 @@ import org.slf4j.LoggerFactory;
 import com.github.openjson.JSONObject;
 
 /**
- * Bean object dynamically created representing a conference room on the MediaServer
+ * Dynamically created object representing a conference room on the MediaServer
  *
  */
 public class KRoom {
 	private static final Logger log = LoggerFactory.getLogger(KRoom.class);
 
-	/**
-	 * Not injected by annotation but by constructor.
-	 */
 	private final StreamProcessor processor;
 	private final RecordingChunkDao chunkDao;
 	private final Room room;
@@ -104,8 +98,6 @@ public class KRoom {
 				.put("uid", stream.getUid())
 				.toString()
 			);
-		//FIXME TODO check close on stop sharing
-		//FIXME TODO permission can be removed, some listener might be required
 	}
 
 	public boolean isRecording() {
@@ -251,8 +243,22 @@ public class KRoom {
 
 	public void updateSipCount(final long count) {
 		if (count != sipCount) {
-			sipCount = count;
 			processor.getByRoom(room.getId()).forEach(stream -> stream.addSipProcessor(count));
+			if (sipCount == 0) {
+				processor.getClientManager()
+					.streamByRoom(room.getId())
+					.filter(Client::isSip)
+					.findAny()
+					.ifPresent(c -> {
+						StreamDesc sd = c.addStream(StreamType.WEBCAM, Activity.AUDIO, Activity.VIDEO); // TODO check this
+						sd.setWidth(120).setHeight(90);
+						c.restoreActivities(sd);
+						KStream stream = join(sd, processor.getHandler());
+						stream.startBroadcast(sd, "", () -> {});
+						processor.getClientManager().update(c);
+					});
+			}
+			sipCount = count;
 		}
 	}
 
diff --git a/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KStream.java b/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KStream.java
index c7fb37a..ffd91e1 100644
--- a/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KStream.java
+++ b/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KStream.java
@@ -40,6 +40,7 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
 
 import org.apache.openmeetings.core.sip.ISipCallbacks;
 import org.apache.openmeetings.core.sip.SipStackProcessor;
@@ -52,6 +53,7 @@ import org.apache.openmeetings.db.entity.record.RecordingChunk.Type;
 import org.apache.openmeetings.db.util.ws.RoomMessage;
 import org.apache.openmeetings.db.util.ws.TextRoomMessage;
 import org.apache.openmeetings.util.OmFileHelper;
+import org.kurento.client.BaseRtpEndpoint;
 import org.kurento.client.Continuation;
 import org.kurento.client.IceCandidate;
 import org.kurento.client.ListenerSubscription;
@@ -60,6 +62,7 @@ import org.kurento.client.MediaObject;
 import org.kurento.client.MediaPipeline;
 import org.kurento.client.MediaProfileSpecType;
 import org.kurento.client.MediaType;
+import org.kurento.client.OfferOptions;
 import org.kurento.client.RecorderEndpoint;
 import org.kurento.client.RtpEndpoint;
 import org.kurento.client.WebRtcEndpoint;
@@ -79,7 +82,7 @@ public class KStream extends AbstractStream implements ISipCallbacks {
 	private MediaProfileSpecType profile;
 	private MediaPipeline pipeline;
 	private RecorderEndpoint recorder;
-	private WebRtcEndpoint outgoingMedia = null;
+	private BaseRtpEndpoint outgoingMedia = null;
 	private RtpEndpoint rtpEndpoint;
 	private Optional<SipStackProcessor> sipProcessor = Optional.empty();
 	private final ConcurrentMap<String, WebRtcEndpoint> listeners = new ConcurrentHashMap<>();
@@ -143,7 +146,13 @@ public class KStream extends AbstractStream implements ISipCallbacks {
 		pipeline = kHandler.createPipiline(Map.of(TAG_ROOM, String.valueOf(getRoomId()), TAG_STREAM_UID, sd.getUid()), new Continuation<Void>() {
 			@Override
 			public void onSuccess(Void result) throws Exception {
-				internalStartBroadcast(sd, sdpOffer);
+				if (sipClient) {
+					addSipProcessor(1);
+				} else {
+					outgoingMedia = createEndpoint(sd.getSid(), sd.getUid(), true);
+					internalStartBroadcast(sd, sdpOffer);
+					notifyOnNewStream(sd);
+				}
 				then.run();
 			}
 
@@ -155,10 +164,10 @@ public class KStream extends AbstractStream implements ISipCallbacks {
 	}
 
 	private void internalStartBroadcast(final StreamDesc sd, final String sdpOffer) throws Exception {
-		outgoingMedia = createEndpoint(sd.getSid(), sd.getUid());
 		outgoingMedia.addMediaSessionTerminatedListener(evt -> log.warn("Media stream terminated {}", sd));
 		flowoutSubscription = outgoingMedia.addMediaFlowOutStateChangeListener(evt -> {
-			log.info("Media Flow STATE :: {}, type {}, evt {}", evt.getState(), evt.getType(), evt.getMediaType());
+			log.info("Media Flow OUT STATE :: {}, evt {}, source {}"
+					, evt.getState(), evt.getMediaType(), evt.getSource());
 			if (MediaFlowState.NOT_FLOWING == evt.getState()) {
 				log.warn("FlowOut Future is created");
 				flowoutFuture = Optional.of(new CompletableFuture<>().completeAsync(() -> {
@@ -173,12 +182,18 @@ public class KStream extends AbstractStream implements ISipCallbacks {
 				dropFlowoutFuture();
 			}
 		});
-		outgoingMedia.addMediaFlowInStateChangeListener(evt -> log.warn("Media FlowIn :: {}", evt));
-		addListener(sd.getSid(), sd.getUid(), sdpOffer);
-		addSipProcessor(kRoom.getSipCount());
+		outgoingMedia.addMediaFlowInStateChangeListener(evt -> log.warn("Media Flow IN :: {}, {}, {}"
+				, evt.getState(), evt.getMediaType(), evt.getSource()));
+		if (!sipClient) {
+			addListener(sd.getSid(), sd.getUid(), sdpOffer);
+			addSipProcessor(kRoom.getSipCount());
+		}
 		if (kRoom.isRecording()) {
 			startRecord();
 		}
+	}
+
+	private void notifyOnNewStream(final StreamDesc sd) {
 		Client c = sd.getClient();
 		WebSocketHelper.sendRoom(new TextRoomMessage(c.getRoomId(), c, RoomMessage.Type.RIGHT_UPDATED, c.getUid()));
 		if (hasAudio || hasVideo || hasScreen) {
@@ -213,11 +228,13 @@ public class KStream extends AbstractStream implements ISipCallbacks {
 			return;
 		}
 
-		final WebRtcEndpoint endpoint = getEndpointForUser(sid, uid);
+		final BaseRtpEndpoint endpoint = getEndpointForUser(sid, uid);
 		final String sdpAnswer = endpoint.processOffer(sdpOffer);
 
-		log.debug("gather candidates");
-		endpoint.gatherCandidates(); // this one might throw Exception
+		if (endpoint instanceof WebRtcEndpoint) {
+			log.debug("gather candidates");
+			((WebRtcEndpoint)endpoint).gatherCandidates(); // this one might throw Exception
+		}
 		log.trace("USER {}: SdpAnswer is {}", this.uid, sdpAnswer);
 		kHandler.sendClient(sid, newKurentoMsg()
 				.put("id", "videoResponse")
@@ -225,7 +242,7 @@ public class KStream extends AbstractStream implements ISipCallbacks {
 				.put("sdpAnswer", sdpAnswer));
 	}
 
-	private WebRtcEndpoint getEndpointForUser(String sid, String uid) {
+	private BaseRtpEndpoint getEndpointForUser(String sid, String uid) {
 		if (uid.equals(this.uid)) {
 			log.debug("PARTICIPANT {}: configuring loopback", this.uid);
 			return outgoingMedia;
@@ -238,7 +255,7 @@ public class KStream extends AbstractStream implements ISipCallbacks {
 			listener.release();
 		}
 		log.debug("PARTICIPANT {}: creating new endpoint for {}", uid, this.uid);
-		listener = createEndpoint(sid, uid);
+		listener = createEndpoint(sid, uid, false);
 		listeners.put(uid, listener);
 
 		log.debug("PARTICIPANT {}: obtained endpoint for {}", uid, this.uid);
@@ -266,14 +283,16 @@ public class KStream extends AbstractStream implements ISipCallbacks {
 		endpoint.addTag("uid", uid);
 	}
 
-	private RtpEndpoint getRtpEndpoint(MediaPipeline pipeline) {
-		RtpEndpoint endpoint = new RtpEndpoint.Builder(pipeline).build();
+	private RtpEndpoint getRtpEndpoint(MediaPipeline pipeline, String direction) {
+		RtpEndpoint endpoint = new RtpEndpoint.Builder(pipeline)
+				//.withProperties(Properties.of(direction, Boolean.TRUE))
+				.build();
 		setTags(endpoint, uid);
 		return endpoint;
 	}
 
-	private WebRtcEndpoint createEndpoint(String sid, String uid) {
-		WebRtcEndpoint endpoint = createWebRtcEndpoint(pipeline);
+	private WebRtcEndpoint createEndpoint(String sid, String uid, boolean send) {
+		WebRtcEndpoint endpoint = createWebRtcEndpoint(pipeline, send);
 		setTags(endpoint, uid);
 
 		endpoint.addIceCandidateFoundListener(evt -> kHandler.sendClient(sid
@@ -475,10 +494,10 @@ public class KStream extends AbstractStream implements ISipCallbacks {
 
 	public void addCandidate(IceCandidate candidate, String uid) {
 		if (this.uid.equals(uid)) {
-			if (outgoingMedia == null) {
+			if (outgoingMedia == null || !(outgoingMedia instanceof WebRtcEndpoint)) {
 				return;
 			}
-			outgoingMedia.addIceCandidate(candidate);
+			((WebRtcEndpoint)outgoingMedia).addIceCandidate(candidate);
 		} else {
 			WebRtcEndpoint endpoint = listeners.get(uid);
 			log.debug("Add candidate for {}, listener found ? {}", uid, endpoint != null);
@@ -488,46 +507,14 @@ public class KStream extends AbstractStream implements ISipCallbacks {
 		}
 	}
 
-	void addSipProcessor(long count) {
-		if (count > 0) {
-			if (sipProcessor.isEmpty()) {
-				try {
-					sipProcessor = kHandler.getSipManager().createSipStackProcessor(
-							randomUUID().toString()
-							, kRoom.getRoom()
-							, this);
-					sipProcessor.ifPresent(SipStackProcessor::register);
-				} catch (Exception e) {
-					log.error("Unexpected error while creating SipProcessor", e);
-				}
-			}
-		} else {
-			releaseRtp();
-		}
-	}
-
 	private static JSONObject convert(com.google.gson.JsonObject o) {
 		return new JSONObject(o.toString());
 	}
 
-	@Override
-	public String getSid() {
-		return sid;
-	}
-
-	@Override
-	public String getUid() {
-		return uid;
-	}
-
 	public Date getConnectedSince() {
 		return connectedSince;
 	}
 
-	public KRoom getKRoom() {
-		return kRoom;
-	}
-
 	public Long getRoomId() {
 		return kRoom.getRoom().getId();
 	}
@@ -548,10 +535,6 @@ public class KStream extends AbstractStream implements ISipCallbacks {
 		return recorder;
 	}
 
-	public WebRtcEndpoint getOutgoingMedia() {
-		return outgoingMedia;
-	}
-
 	public Long getChunkId() {
 		return chunkId;
 	}
@@ -571,26 +554,61 @@ public class KStream extends AbstractStream implements ISipCallbacks {
 				+ flowoutFuture + ", chunkId=" + chunkId + ", type=" + type + ", sid=" + sid + ", uid=" + uid + "]";
 	}
 
+	void addSipProcessor(long count) {
+		if (count > 0) {
+			if (sipProcessor.isEmpty()) {
+				try {
+					sipProcessor = kHandler.getSipManager().createSipStackProcessor(
+							randomUUID().toString()
+							, kRoom.getRoom()
+							, this);
+					sipProcessor.ifPresent(SipStackProcessor::register);
+				} catch (Exception e) {
+					log.error("Unexpected error while creating SipProcessor", e);
+				}
+			}
+		} else {
+			if (sipClient) {
+				release();
+			} else {
+				releaseRtp();
+			}
+		}
+	}
+
 	@Override
 	public void onRegisterOk() {
-		if (sipClient) {
-
-		} else {
-			rtpEndpoint = getRtpEndpoint(pipeline);
+		rtpEndpoint = getRtpEndpoint(pipeline, sipClient ? "recvonly" : "sendonly");
+		OfferOptions options = new OfferOptions();
+		options.setOfferToReceiveAudio(hasAudio);
+		options.setOfferToReceiveVideo(hasVideo);
+		String offer = null;
+		if (!sipClient) {
+			offer = rtpEndpoint.generateOffer(options);
 			if (hasAudio) {
 				outgoingMedia.connect(rtpEndpoint, MediaType.AUDIO);
 			}
 			if (hasVideo) {
 				outgoingMedia.connect(rtpEndpoint, MediaType.VIDEO);
 			}
-			sipProcessor.get().invite(kRoom.getRoom(), rtpEndpoint.generateOffer());
 		}
+		sipProcessor.get().invite(kRoom.getRoom(), offer);
 	}
 
 	@Override
-	public void onInviteOk(String sdp) {
+	public void onInviteOk(String sdp, Consumer<String> answerConsumer) {
 		if (sipClient) {
-
+			String answer = rtpEndpoint.processOffer(sdp);
+			answerConsumer.accept(answer);
+			log.debug(answer);
+			StreamDesc sd = kHandler.getStreamProcessor().getBySid(sid).getStream(uid);
+			try {
+				outgoingMedia = rtpEndpoint;
+				internalStartBroadcast(sd, sdp);
+				notifyOnNewStream(sd);
+			} catch (Exception e) {
+				log.error("Unexpected error");
+			}
 		} else {
 			rtpEndpoint.processAnswer(sdp);
 		}
diff --git a/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KTestStream.java b/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KTestStream.java
index 6949528..741c1f6 100644
--- a/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KTestStream.java
+++ b/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KTestStream.java
@@ -72,7 +72,7 @@ public class KTestStream extends AbstractStream {
 	}
 
 	private void startTestRecording(IWsClient c, JSONObject msg) {
-		webRtcEndpoint = createWebRtcEndpoint(pipeline);
+		webRtcEndpoint = createWebRtcEndpoint(pipeline, null);
 		webRtcEndpoint.connect(webRtcEndpoint);
 
 		MediaProfileSpecType profile = getProfile(msg);
@@ -134,7 +134,7 @@ public class KTestStream extends AbstractStream {
 
 	public void play(final IWsClient inClient, JSONObject msg) {
 		createPipeline(() -> {
-			webRtcEndpoint = createWebRtcEndpoint(pipeline);
+			webRtcEndpoint = createWebRtcEndpoint(pipeline, true);
 			player = createPlayerEndpoint(pipeline, recPath);
 			player.connect(webRtcEndpoint);
 			webRtcEndpoint.addMediaSessionStartedListener(evt -> {
diff --git a/openmeetings-core/src/main/java/org/apache/openmeetings/core/sip/ISipCallbacks.java b/openmeetings-core/src/main/java/org/apache/openmeetings/core/sip/ISipCallbacks.java
index a04f275..b476c8a 100644
--- a/openmeetings-core/src/main/java/org/apache/openmeetings/core/sip/ISipCallbacks.java
+++ b/openmeetings-core/src/main/java/org/apache/openmeetings/core/sip/ISipCallbacks.java
@@ -18,7 +18,9 @@
  */
 package org.apache.openmeetings.core.sip;
 
+import java.util.function.Consumer;
+
 public interface ISipCallbacks {
 	void onRegisterOk();
-	void onInviteOk(String sdp);
+	void onInviteOk(String sdp, Consumer<String> answerConsumer);
 }
diff --git a/openmeetings-core/src/main/java/org/apache/openmeetings/core/sip/SipManager.java b/openmeetings-core/src/main/java/org/apache/openmeetings/core/sip/SipManager.java
index ac8fc06..a95e419 100644
--- a/openmeetings-core/src/main/java/org/apache/openmeetings/core/sip/SipManager.java
+++ b/openmeetings-core/src/main/java/org/apache/openmeetings/core/sip/SipManager.java
@@ -26,6 +26,7 @@ import java.util.Optional;
 import java.util.function.Function;
 
 import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
 
 import org.apache.openmeetings.db.entity.room.Room;
 import org.apache.openmeetings.db.entity.user.User;
@@ -35,6 +36,7 @@ import org.apache.wicket.util.string.Strings;
 import org.asteriskjava.manager.DefaultManagerConnection;
 import org.asteriskjava.manager.ManagerConnection;
 import org.asteriskjava.manager.ManagerConnectionFactory;
+import org.asteriskjava.manager.ManagerConnectionState;
 import org.asteriskjava.manager.ResponseEvents;
 import org.asteriskjava.manager.action.ConfbridgeListAction;
 import org.asteriskjava.manager.action.DbDelAction;
@@ -85,6 +87,7 @@ public class SipManager implements ISipManager {
 	private int maxLocalWsPort = 7666;
 
 	private ManagerConnectionFactory factory;
+	private ManagerConnection con;
 	private String sipUserPicture;
 	private BitSet ports;
 
@@ -100,13 +103,23 @@ public class SipManager implements ISipManager {
 		}
 	}
 
-	private ManagerConnection getConnection() {
-		DefaultManagerConnection con = (DefaultManagerConnection)factory.createManagerConnection();
-		con.setDefaultEventTimeout(managerTimeout);
-		con.setDefaultResponseTimeout(managerTimeout);
-		con.setSocketReadTimeout((int)managerTimeout);
-		con.setSocketTimeout((int)managerTimeout);
-		return con;
+	@PreDestroy
+	public void destroy() {
+		if (con != null) {
+			con.logoff();
+		}
+	}
+
+	private void connectManager() throws Exception {
+		if (con == null || ManagerConnectionState.CONNECTED != con.getState()) {
+			DefaultManagerConnection defCon = (DefaultManagerConnection)factory.createManagerConnection();
+			defCon.setDefaultEventTimeout(managerTimeout);
+			defCon.setDefaultResponseTimeout(managerTimeout);
+			defCon.setSocketReadTimeout((int)managerTimeout);
+			defCon.setSocketTimeout((int)managerTimeout);
+			con = defCon;
+			con.login("on");
+		}
 	}
 
 	private ManagerResponse exec(ManagerAction action) {
@@ -114,9 +127,8 @@ public class SipManager implements ISipManager {
 			log.warn("There is no Asterisk configured");
 			return null;
 		}
-		ManagerConnection con = getConnection();
 		try {
-			con.login();
+			connectManager();
 			ManagerResponse r = con.sendAction(action);
 			if (r != null) {
 				log.debug("{}", r);
@@ -124,12 +136,6 @@ public class SipManager implements ISipManager {
 			return (r instanceof ManagerError) ? null : r;
 		} catch (Exception e) {
 			log.error("Error while executing ManagerAction: {}", action, e);
-		} finally {
-			try {
-				con.logoff();
-			} catch (Exception e) {
-				// no-op
-			}
 		}
 		return null;
 	}
@@ -139,22 +145,15 @@ public class SipManager implements ISipManager {
 			log.warn("There is no Asterisk configured");
 			return null;
 		}
-		ManagerConnection con = getConnection();
 		try {
-			con.login("on");
+			connectManager();
 			ResponseEvents r = con.sendEventGeneratingAction(action);
 			if (r != null) {
-				log.debug("{}", r.getResponse());
+				log.trace("{}", r.getResponse());
 			}
 			return (r == null || r.getResponse() instanceof ManagerError) ? null : r;
 		} catch (Exception e) {
 			log.error("Error while executing EventGeneratingAction: {}", action, e);
-		} finally {
-			try {
-				con.logoff();
-			} catch (Exception e) {
-				// no-op
-			}
 		}
 		return null;
 	}
diff --git a/openmeetings-core/src/main/java/org/apache/openmeetings/core/sip/SipStackProcessor.java b/openmeetings-core/src/main/java/org/apache/openmeetings/core/sip/SipStackProcessor.java
index 31a1941..dc22235 100644
--- a/openmeetings-core/src/main/java/org/apache/openmeetings/core/sip/SipStackProcessor.java
+++ b/openmeetings-core/src/main/java/org/apache/openmeetings/core/sip/SipStackProcessor.java
@@ -198,8 +198,7 @@ public class SipStackProcessor implements SipListenerExt {
 					callbacks.onRegisterOk();
 				} else if (INVITE.equals(prevReq.getMethod())) {
 					dialog = evt.getDialog();
-					ack(evt);
-					callbacks.onInviteOk(new String((byte[])resp.getContent()));
+					callbacks.onInviteOk(new String((byte[])resp.getContent()), answer -> ack(evt, answer));
 				} else if (BYE.equals(prevReq.getMethod())) {
 					doDestroy();
 				}
@@ -212,12 +211,13 @@ public class SipStackProcessor implements SipListenerExt {
 		}
 	}
 
-	private void ack(ResponseEvent evt) {
+	private void ack(ResponseEvent evt, String answer) {
 		try {
 			Response resp = evt.getResponse();
 			Dialog dlg = evt.getDialog();
 			CSeqHeader cseqHead = (CSeqHeader)resp.getHeader(CSeqHeader.NAME);
 			Request ack = dlg.createAck(cseqHead.getSeqNumber());
+			addSdp(ack, answer);
 			dlg.sendAck(ack);
 		} catch (Exception e) {
 			log.error("ack {}", evt, e);
@@ -336,6 +336,13 @@ public class SipStackProcessor implements SipListenerExt {
 				});
 	}
 
+	private void addSdp(Request req, String sdp) throws Exception {
+		if (sdp != null) {
+			req.addHeader(headerFactory.createContentLengthHeader(sdp.length()));
+			req.setContent(sdp, headerFactory.createContentTypeHeader("application", "sdp"));
+		}
+	}
+
 	public void invite(Room r, String sdp) {
 		final String sipNumber = getSipNumber(r);
 		if (sipNumber == null) {
@@ -349,10 +356,7 @@ public class SipStackProcessor implements SipListenerExt {
 				, req -> {
 					try {
 						addAllow(req);
-						if (sdp != null) {
-							req.addHeader(headerFactory.createContentLengthHeader(sdp.length()));
-							req.setContent(sdp, headerFactory.createContentTypeHeader("application", "sdp"));
-						}
+						addSdp(req, sdp);
 					} catch (Exception e) {
 						log.error("fail patch invite request", e);
 					}
diff --git a/openmeetings-core/src/test/java/org/apache/openmeetings/core/remote/BaseMockedTest.java b/openmeetings-core/src/test/java/org/apache/openmeetings/core/remote/BaseMockedTest.java
index 7402754..3565a64 100644
--- a/openmeetings-core/src/test/java/org/apache/openmeetings/core/remote/BaseMockedTest.java
+++ b/openmeetings-core/src/test/java/org/apache/openmeetings/core/remote/BaseMockedTest.java
@@ -20,6 +20,7 @@
 package org.apache.openmeetings.core.remote;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.mock;
@@ -94,7 +95,7 @@ public class BaseMockedTest {
 					return null;
 				}
 			});
-			streamMock.when(() -> AbstractStream.createWebRtcEndpoint(any(MediaPipeline.class))).thenReturn(mock(WebRtcEndpoint.class));
+			streamMock.when(() -> AbstractStream.createWebRtcEndpoint(any(MediaPipeline.class), anyBoolean())).thenReturn(mock(WebRtcEndpoint.class));
 			streamMock.when(() -> AbstractStream.createRecorderEndpoint(any(MediaPipeline.class), anyString(), any(MediaProfileSpecType.class))).thenReturn(mock(RecorderEndpoint.class));
 			streamMock.when(() -> AbstractStream.createPlayerEndpoint(any(MediaPipeline.class), anyString())).thenReturn(mock(PlayerEndpoint.class));
 
diff --git a/openmeetings-server/src/site/markdown/AsteriskIntegration.md b/openmeetings-server/src/site/markdown/AsteriskIntegration.md
index 0b98140..c6459d4 100644
--- a/openmeetings-server/src/site/markdown/AsteriskIntegration.md
+++ b/openmeetings-server/src/site/markdown/AsteriskIntegration.md
@@ -138,7 +138,6 @@ encryption=no
 avpf=yes
 icesupport=yes
 directmedia=no
-disallow=all
 allow=!all,ulaw,opus,vp8
 ```
 
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/connection/KStreamDto.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/connection/KStreamDto.java
index 90cc2b7..75265a7 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/connection/KStreamDto.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/connection/KStreamDto.java
@@ -37,30 +37,26 @@ import org.apache.openmeetings.db.entity.record.RecordingChunk.Type;
 public class KStreamDto implements IDataProviderEntity {
 	private static final long serialVersionUID = 1L;
 
-	private String sid;
-	private String uid;
-	private Long roomId;
-	private Date connectedSince;
-	private StreamType streamType;
-	private String profile;
-	private String recorder;
-	private Long chunkId;
-	private Type type;
+	private final String sid;
+	private final String uid;
+	private final Long roomId;
+	private final Date connectedSince;
+	private final StreamType streamType;
+	private final String profile;
+	private final String recorder;
+	private final Long chunkId;
+	private final Type type;
 
 	public KStreamDto(KStream kStream) {
-		this.sid = kStream.getSid();
-		this.uid = kStream.getUid();
-		this.roomId = kStream.getRoomId();
-		this.connectedSince = kStream.getConnectedSince();
-		this.streamType = kStream.getStreamType();
-		this.profile = kStream.getProfile().toString();
-		this.recorder = (kStream.getRecorder() == null) ? null : kStream.getRecorder().toString();
-		this.chunkId = kStream.getChunkId();
-		this.type = kStream.getType();
-	}
-
-	public static long getSerialversionuid() {
-		return serialVersionUID;
+		sid = kStream.getSid();
+		uid = kStream.getUid();
+		roomId = kStream.getRoomId();
+		connectedSince = kStream.getConnectedSince();
+		streamType = kStream.getStreamType();
+		profile = kStream.getProfile().toString();
+		recorder = (kStream.getRecorder() == null) ? null : kStream.getRecorder().toString();
+		chunkId = kStream.getChunkId();
+		type = kStream.getType();
 	}
 
 	public String getSid() {
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 422755a..6829c0f 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
@@ -102,7 +102,6 @@ public class TimerService {
 	private void updateSipLastName(Optional<Client> sipClient, Room r) {
 		long count = sipManager.countUsers(r.getConfno());
 		final String newLastName = "(" + count + ")";
-		kHandler.updateSipCount(r, count);
 		sipClient.ifPresentOrElse(c -> {
 			if (!newLastName.equals(c.getUser().getLastname())) {
 				c.getUser().setLastname(newLastName).resetDisplayName();
@@ -113,11 +112,13 @@ public class TimerService {
 			User sipUser = sipManager.getSipUser(r);
 			sipUser.setLastname(newLastName).resetDisplayName();
 			Client c = new Client("-- unique - sip - session --", 1, sipUser, sipUser.getPictureUri());
+			c.allow(Right.VIDEO, Right.AUDIO);
 			cm.add(c);
 			c.setRoom(r);
 			cm.addToRoom(c);
 			WebSocketHelper.sendRoom(new TextRoomMessage(r.getId(), c, RoomMessage.Type.ROOM_ENTER, c.getUid()));
 		});
+		kHandler.updateSipCount(r, count);
 	}
 
 	public void scheduleModCheck(Room r) {
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-room.js b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-room.js
index b54fbf6..0d349c2 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-room.js
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-room.js
@@ -440,25 +440,27 @@ var Room = (function() {
 			.css('background-image', 'url(' + c.user.pictureUri + ')')
 			.find('.user.name').text(c.user.displayName);
 
-		const actions = le.find('.user.actions');
-		__rightVideoIcon(c, actions);
-		__rightAudioIcon(c, actions);
-		__rightOtherIcons(c, actions);
-		__activityIcon(actions, '.kick'
-			, () => !self && _hasRight('MODERATOR') && !_hasRight('SUPER_MODERATOR', c.rights)
-			, null
-			, {
-				confirmationEvent: 'om-kick'
-				, placement: Settings.isRtl ? 'left' : 'right'
-				, onConfirm: () => OmUtil.roomAction({action: 'kick', uid: c.uid})
-			});
-		__activityIcon(actions, '.private-chat'
+		if (c.user.id !== -1) {
+			const actions = le.find('.user.actions');
+			__rightVideoIcon(c, actions);
+			__rightAudioIcon(c, actions);
+			__rightOtherIcons(c, actions);
+			__activityIcon(actions, '.kick'
+				, () => !self && _hasRight('MODERATOR') && !_hasRight('SUPER_MODERATOR', c.rights)
+				, null
+				, {
+					confirmationEvent: 'om-kick'
+					, placement: Settings.isRtl ? 'left' : 'right'
+					, onConfirm: () => OmUtil.roomAction({action: 'kick', uid: c.uid})
+				});
+			__activityIcon(actions, '.private-chat'
 				, () => options.userId !== c.user.id && $('#chatPanel').is(':visible')
 				, function() {
 					Chat.addTab('chatTab-u' + c.user.id, c.user.displayName);
 					Chat.open();
 					$('#chatMessage .wysiwyg-editor').click();
 				});
+		}
 		if (self) {
 			options.rights = c.rights;
 			_setQuickPollRights();