diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/EndReason.java b/openvidu-server/src/main/java/io/openvidu/server/core/EndReason.java index f2112189..8333e1eb 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/EndReason.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/EndReason.java @@ -66,10 +66,9 @@ public enum EndReason { /** * The last participant left the session, which caused the session to be closed. - * Applies to events webrtcConnectionDestroyed, participantLeft, - * recordingStatusChanged and sessionDestroyed. Can be triggered from other - * events with other end reasons (disconnect, forceDisconnectByUser, - * forceDisconnectByServer, networkDisconnect) + * Applies to events recordingStatusChanged, broadcastStopped and + * sessionDestroyed. Can be triggered from other events with other end reasons + * (disconnect, forceDisconnectByServer, networkDisconnect) */ lastParticipantLeft, @@ -79,10 +78,16 @@ public enum EndReason { */ recordingStoppedByServer, + /** + * The server application called the REST operation to stop a broadcast. Applies + * to event broadcastStopped + */ + broadcastStoppedByServer, + /** * The server application called the REST operation to close a session. Applies - * to events webrtcConnectionDestroyed, participantLeft, recordingStatusChanged - * and sessionDestroyed + * to events webrtcConnectionDestroyed, participantLeft, recordingStatusChanged, + * broadcastStopped and sessionDestroyed */ sessionClosedByServer, @@ -96,8 +101,8 @@ public enum EndReason { /** * A media server disconnected. This is reserved for Media Nodes being * gracefully removed from an OpenVidu Pro cluster. Applies to events - * webrtcConnectionDestroyed, participantLeft, recordingStatusChanged and - * sessionDestroyed + * webrtcConnectionDestroyed, participantLeft, recordingStatusChanged, + * broadcastStopped and sessionDestroyed */ mediaServerDisconnect, @@ -110,22 +115,24 @@ public enum EndReason { /** * A node has crashed. For now this means a Media Node has crashed. Applies to - * events webrtcConnectionDestroyed, participantLeft, recordingStatusChanged and - * sessionDestroyed + * events webrtcConnectionDestroyed, participantLeft, recordingStatusChanged, + * broadcastStopped and sessionDestroyed */ nodeCrashed, /** * OpenVidu Server has gracefully stopped. This is reserved for OpenVidu Pro * restart operation. Applies to events webrtcConnectionDestroyed, - * participantLeft, recordingStatusChanged and sessionDestroyed + * participantLeft, recordingStatusChanged, broadcastStopped and + * sessionDestroyed */ openviduServerStopped, /** * A recording has been stopped automatically * (https://docs.openvidu.io/en/stable/advanced-features/recording/#automatic-stop-of-recordings). - * Applies to event recordingStatusChanged + * Applies to event recordingStatusChanged and sessionDestroyed (in case the + * session was inactive except for the recording) */ automaticStop diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java b/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java index 6bf5fbdb..d7152a52 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java @@ -192,7 +192,7 @@ public abstract class SessionManager { public abstract void onUnsubscribeFromSpeechToText(Participant participant, Integer transactionId, String connectionId); - public abstract void stopBroadcastIfNecessary(Session session); + public abstract void stopBroadcastIfNecessary(Session session, EndReason reason); public void onEcho(String participantPrivateId, Integer requestId) { sessionEventsHandler.onEcho(participantPrivateId, requestId); diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionManager.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionManager.java index ae737af1..ce1945c2 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionManager.java @@ -262,6 +262,8 @@ public class KurentoSessionManager extends SessionManager { // "SessionManager.closeSessionAndEmptyCollections" if (!EndReason.sessionClosedByServer.equals(reason)) { + final long autoStopTimeout = this.openviduConfig.getOpenviduRecordingAutostopTimeout(); + if (remainingParticipants.isEmpty()) { if (this.recordingManager.sessionIsBeingRecorded(sessionId)) { // Start countdown to stop recording. Will be aborted if a Publisher starts @@ -269,8 +271,9 @@ public class KurentoSessionManager extends SessionManager { // recordings it would still remain a recorder participant log.info( "Last participant left. Starting {} seconds countdown for stopping recording of session {}", - this.openviduConfig.getOpenviduRecordingAutostopTimeout(), sessionId); - recordingManager.initAutomaticRecordingStopThread(session); + autoStopTimeout, sessionId); + recordingManager.initAutomaticRecordingStopThread(session, + autoStopTimeout == 0 ? EndReason.lastParticipantLeft : EndReason.automaticStop); } else { try { if (session.closingLock.writeLock().tryLock(15, TimeUnit.SECONDS)) { @@ -310,8 +313,10 @@ public class KurentoSessionManager extends SessionManager { // RECORDER or STT participant is the last one standing. Start countdown log.info( "Last participant left. Starting {} seconds countdown for stopping recording of session {}", - this.openviduConfig.getOpenviduRecordingAutostopTimeout(), sessionId); - recordingManager.initAutomaticRecordingStopThread(session); + autoStopTimeout, sessionId); + recordingManager.initAutomaticRecordingStopThread(session, + autoStopTimeout == 0 ? EndReason.lastParticipantLeft + : EndReason.automaticStop); } else if (session.getSessionProperties().defaultRecordingProperties().outputMode() .equals(Recording.OutputMode.COMPOSED_QUICK_START)) { @@ -329,7 +334,7 @@ public class KurentoSessionManager extends SessionManager { && remainingParticipants.stream().anyMatch(p -> p.isBroadcastParticipant()); if (broadcastParticipantLeft) { - this.stopBroadcastIfNecessary(session); + this.stopBroadcastIfNecessary(session, reason); } } } @@ -1425,7 +1430,7 @@ public class KurentoSessionManager extends SessionManager { } @Override - public void stopBroadcastIfNecessary(Session session) { + public void stopBroadcastIfNecessary(Session session, EndReason reason) { } private io.openvidu.server.recording.Recording getActiveRecordingIfAllowedByParticipantRole( diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManager.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManager.java index 1d2bd110..5edaf35d 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManager.java @@ -316,7 +316,7 @@ public class RecordingManager { "No publisher in session {}. Starting {} seconds countdown for stopping recording", session.getSessionId(), this.openviduConfig.getOpenviduRecordingAutostopTimeout()); - this.initAutomaticRecordingStopThread(session); + this.initAutomaticRecordingStopThread(session, EndReason.automaticStop); } return recording; } @@ -676,7 +676,7 @@ public class RecordingManager { return recordingManagerUtils.getRecordingUrl(recording); } - public void initAutomaticRecordingStopThread(final Session session) { + public void initAutomaticRecordingStopThread(final Session session, final EndReason reason) { final String recordingId = this.sessionsRecordings.get(session.getSessionId()).getId(); this.automaticRecordingStopThreads.computeIfAbsent(session.getSessionId(), f -> { @@ -695,12 +695,11 @@ public class RecordingManager { } if (session.getParticipants().size() == 0 || session.onlyRecorderAndOrSttAndOrBroadcastParticipant()) { - // Close session if there are no participants connected (RECORDER/STT/BROADCAST do - // not count) and publishing + // Close session if there are no participants connected (RECORDER/STT/BROADCAST + // do not count) and publishing log.info("Closing session {} after automatic stop of recording {}", session.getSessionId(), recordingId); - sessionManager.closeSessionAndEmptyCollections(session, EndReason.automaticStop, - true); + sessionManager.closeSessionAndEmptyCollections(session, reason, true); } else { // There are users connected, but no one is publishing // We don't need the lock if session is not closing @@ -709,7 +708,7 @@ public class RecordingManager { log.info( "Automatic stopping recording {}. There are users connected to session {}, but no one is publishing", recordingId, session.getSessionId()); - this.stopRecording(session, recordingId, EndReason.automaticStop); + this.stopRecording(session, recordingId, reason); } } finally { if (!alreadyUnlocked) { @@ -750,8 +749,9 @@ public class RecordingManager { if (session.getParticipants().size() == 0 || session.onlyRecorderAndOrSttAndOrBroadcastParticipant()) { // Close session if there are no participants connected (except for - // RECORDER/STT/BROADCAST). This code will only be executed if recording is manually - // stopped during the automatic stop timeout, so the session must be also closed + // RECORDER/STT/BROADCAST). This code will only be executed if recording is + // manually stopped during the automatic stop timeout, so the session must be + // also closed log.info( "Ongoing recording of session {} was explicetly stopped within timeout for automatic recording stop. Closing session", session.getSessionId()); diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingService.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingService.java index 12d6627f..5e886892 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingService.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingService.java @@ -217,11 +217,9 @@ public abstract class RecordingService { protected void uploadRecording(final Recording recording, EndReason reason) { recordingUploader.uploadRecording(recording, () -> { - final long timestamp = System.currentTimeMillis(); - cdr.recordRecordingStatusChanged(recording, reason, timestamp, recording.getStatus()); + cdr.recordRecordingStatusChanged(recording, reason, System.currentTimeMillis(), recording.getStatus()); }, () -> { - final long timestamp = System.currentTimeMillis(); - cdr.recordRecordingStatusChanged(recording, reason, timestamp, + cdr.recordRecordingStatusChanged(recording, reason, System.currentTimeMillis(), io.openvidu.java.client.Recording.Status.failed); }); } diff --git a/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/webhook/CustomWebhook.java b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/webhook/CustomWebhook.java index 48024a09..b2ed7207 100644 --- a/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/webhook/CustomWebhook.java +++ b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/utils/webhook/CustomWebhook.java @@ -51,7 +51,7 @@ public class CustomWebhook { public static CountDownLatch initLatch; public static int accumulatedNumberOfEvents = 0; public final static ConcurrentMap> accumulatedEvents = new ConcurrentHashMap<>(); - static final ConcurrentMap> events = new ConcurrentHashMap<>(); + public static final ConcurrentMap> events = new ConcurrentHashMap<>(); static final BlockingQueue eventsInOrder = new LinkedBlockingDeque<>(); public static void main(String[] args, CountDownLatch initLatch) { diff --git a/openvidu-test-e2e/pom.xml b/openvidu-test-e2e/pom.xml index cee8cb7a..2ffb9451 100644 --- a/openvidu-test-e2e/pom.xml +++ b/openvidu-test-e2e/pom.xml @@ -102,6 +102,11 @@ openvidu-java-client ${version.openvidu.java.client} + + com.github.docker-java + docker-java + ${version.dockerjava} + org.testcontainers testcontainers diff --git a/openvidu-test-e2e/src/main/java/io/openvidu/test/e2e/MediaNodeDockerUtils.java b/openvidu-test-e2e/src/main/java/io/openvidu/test/e2e/MediaNodeDockerUtils.java new file mode 100644 index 00000000..30d7c7e1 --- /dev/null +++ b/openvidu-test-e2e/src/main/java/io/openvidu/test/e2e/MediaNodeDockerUtils.java @@ -0,0 +1,61 @@ +package io.openvidu.test.e2e; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.async.ResultCallback; +import com.github.dockerjava.api.command.ExecCreateCmdResponse; +import com.github.dockerjava.core.DefaultDockerClientConfig; +import com.github.dockerjava.core.DockerClientBuilder; +import com.github.dockerjava.core.DockerClientConfig; +import com.github.dockerjava.jaxrs.JerseyDockerHttpClient; +import com.github.dockerjava.transport.DockerHttpClient; + +public class MediaNodeDockerUtils { + + private static final Logger log = LoggerFactory.getLogger(MediaNodeDockerUtils.class); + + public static void crashMediaNode(String containerId) { + log.info("Stopping Media Node container"); + DockerClient dockerClient = getDockerClient(); + dockerClient.removeContainerCmd(containerId).withForce(true).exec(); + } + + public static String getMediaNodeIp(String containerId) { + DockerClient dockerClient = getDockerClient(); + return dockerClient.inspectContainerCmd(containerId).exec().getNetworkSettings().getNetworks().get("bridge") + .getIpAddress(); + } + + public static void crashMediaServerInsideMediaNode(String containerId) { + log.info("Stopping KMS container inside Media Node"); + DockerClient dockerClient = getDockerClient(); + ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId).withAttachStdout(true) + .withAttachStderr(true).withCmd("bash", "-c", "docker rm -f kms").exec(); + + dockerClient.execStartCmd(execCreateCmdResponse.getId()).exec(new ResultCallback.Adapter<>() { + }); + + } + + public static void stopMediaServerInsideMediaNodeAndRecover(String containerId, int millisStop) { + log.info("Stopping and starting KMS container inside Media Node " + containerId + "waiting " + millisStop + + " ms"); + DockerClient dockerClient = getDockerClient(); + ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId).withAttachStdout(true) + .withAttachStderr(true) + .withCmd("bash", "-c", "docker stop kms && sleep " + (millisStop / 1000) + " && docker start kms") + .exec(); + dockerClient.execStartCmd(execCreateCmdResponse.getId()).exec(new ResultCallback.Adapter<>() { + }); + } + + public static DockerClient getDockerClient() { + DockerClientConfig dockerClientConfig = DefaultDockerClientConfig.createDefaultConfigBuilder().build(); + DockerHttpClient dockerHttpClient = new JerseyDockerHttpClient.Builder() + .dockerHost(dockerClientConfig.getDockerHost()).sslConfig(dockerClientConfig.getSSLConfig()).build(); + return DockerClientBuilder.getInstance(dockerClientConfig).withDockerHttpClient(dockerHttpClient).build(); + } + +} diff --git a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduProTestAppE2eTest.java b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduProTestAppE2eTest.java index 0f90e777..aec57ac6 100644 --- a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduProTestAppE2eTest.java +++ b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduProTestAppE2eTest.java @@ -210,6 +210,357 @@ public class OpenViduProTestAppE2eTest extends AbstractOpenViduTestappE2eTest { } } + /** + * Go through every EndReason and test that all affected entities trigger the + * required Webhook events when being destroyed for that specific reason. Check + * that "reason" property of every event is right and that there are no extra + * events triggered. + */ + @Test + @DisplayName("End reason") + void endReasonTest() throws Exception { + + isRecordingTest = true; + + log.info("End reason test"); + + CountDownLatch initLatch = new CountDownLatch(1); + io.openvidu.test.browsers.utils.webhook.CustomWebhook.main(new String[0], initLatch); + + try { + + if (!initLatch.await(30, TimeUnit.SECONDS)) { + Assertions.fail("Timeout waiting for webhook springboot app to start"); + CustomWebhook.shutDown(); + return; + } + + CustomHttpClient restClient = new CustomHttpClient(OpenViduTestAppE2eTest.OPENVIDU_URL, "OPENVIDUAPP", + OpenViduTestAppE2eTest.OPENVIDU_SECRET); + JsonObject config = restClient.rest(HttpMethod.GET, "/openvidu/api/config", HttpURLConnection.HTTP_OK); + + String defaultOpenViduWebhookEndpoint = null; + Integer defaultOpenViduRecordingAutostopTimeout = null; + if (config.has("OPENVIDU_WEBHOOK_ENDPOINT")) { + defaultOpenViduWebhookEndpoint = config.get("OPENVIDU_WEBHOOK_ENDPOINT").getAsString(); + } + if (config.has("OPENVIDU_RECORDING_AUTOSTOP_TIMEOUT")) { + defaultOpenViduRecordingAutostopTimeout = config.get("OPENVIDU_RECORDING_AUTOSTOP_TIMEOUT").getAsInt(); + } + + try { + + Map newConfig = Map.of("OPENVIDU_WEBHOOK", true, "OPENVIDU_WEBHOOK_ENDPOINT", + "http://127.0.0.1:7777/webhook", "OPENVIDU_RECORDING_AUTOSTOP_TIMEOUT", 0); + restartOpenViduServer(newConfig); + + OpenViduTestappUser user = setupBrowserAndConnectToOpenViduTestapp("chrome"); + + // unsubscribe: webrtcConnectionDestroyed + this.connectTwoUsers(user, restClient, false, false, false); + user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 .sub-btn")).click(); + Assertions.assertEquals("unsubscribe", + CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 2).get("reason").getAsString()); + CustomWebhook.events.values().forEach(collection -> Assertions.assertTrue(collection.isEmpty())); + + // unpublish: webrtcConnectionDestroyed + this.connectTwoUsers(user, restClient, false, false, false); + user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 .pub-btn")).click(); + for (int i = 0; i < 2; i++) { + Assertions.assertEquals("unpublish", + CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 2).get("reason").getAsString()); + } + CustomWebhook.events.values().forEach(collection -> Assertions.assertTrue(collection.isEmpty())); + + // disconnect: webrtcConnectionDestroyed, participantLeft (and subsequent + // lastParticipantLeft triggered events for sessionDestroyed, + // recordingStatusChanged, [broadcastStopped]) + this.connectTwoUsers(user, restClient, false, true, true); + // First user out + user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 .leave-btn")).click(); + for (int i = 0; i < 3; i++) { + Assertions.assertEquals("disconnect", + CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 2).get("reason").getAsString()); + } + Assertions.assertEquals("disconnect", + CustomWebhook.waitForEvent("participantLeft", 2).get("reason").getAsString()); + // Second user out + user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 .leave-btn")).click(); + Assertions.assertEquals("disconnect", + CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 2).get("reason").getAsString()); + Assertions.assertEquals("disconnect", + CustomWebhook.waitForEvent("participantLeft", 2).get("reason").getAsString()); + for (int i = 0; i < 2; i++) { + Assertions.assertEquals("lastParticipantLeft", + CustomWebhook.waitForEvent("recordingStatusChanged", 2).get("reason").getAsString()); + } + Assertions.assertEquals("lastParticipantLeft", + CustomWebhook.waitForEvent("sessionDestroyed", 2).get("reason").getAsString()); + CustomWebhook.events.values().forEach(collection -> Assertions.assertTrue(collection.isEmpty())); + + // forceUnpublishByUser: webrtcConnectionDestroyed + this.connectTwoUsers(user, restClient, true, false, false); + user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 .force-unpub-btn")).click(); + for (int i = 0; i < 2; i++) { + Assertions.assertEquals("forceUnpublishByUser", + CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 2).get("reason").getAsString()); + } + CustomWebhook.events.values().forEach(collection -> Assertions.assertTrue(collection.isEmpty())); + + // forceUnpublishByServer: webrtcConnectionDestroyed + this.connectTwoUsers(user, restClient, false, false, false); + String streamId = restClient + .rest(HttpMethod.GET, "/openvidu/api/sessions/TestSession", HttpURLConnection.HTTP_OK) + .get("connections").getAsJsonObject().get("content").getAsJsonArray().asList().stream() + .filter(con -> con.getAsJsonObject().get("role").getAsString().equals("PUBLISHER")).findFirst() + .get().getAsJsonObject().get("publishers").getAsJsonArray().get(0).getAsJsonObject() + .get("streamId").getAsString(); + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/TestSession/stream/" + streamId, + HttpURLConnection.HTTP_NO_CONTENT); + for (int i = 0; i < 2; i++) { + Assertions.assertEquals("forceUnpublishByServer", + CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 2).get("reason").getAsString()); + } + CustomWebhook.events.values().forEach(collection -> Assertions.assertTrue(collection.isEmpty())); + + // forceDisconnectByUser: webrtcConnectionDestroyed, participantLeft + this.connectTwoUsers(user, restClient, true, true, true); + user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 .force-disconnect-btn")).click(); + for (int i = 0; i < 3; i++) { + Assertions.assertEquals("forceDisconnectByUser", + CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 2).get("reason").getAsString()); + } + Assertions.assertEquals("forceDisconnectByUser", + CustomWebhook.waitForEvent("participantLeft", 2).get("reason").getAsString()); + CustomWebhook.events.values().forEach(collection -> Assertions.assertTrue(collection.isEmpty())); + + // forceDisconnectByServer: webrtcConnectionDestroyed, participantLeft (and + // subsequent lastParticipantLeft triggered events for sessionDestroyed, + // recordingStatusChanged, [broadcastStopped]) + this.connectTwoUsers(user, restClient, false, true, true); + String[] connectionIds = restClient + .rest(HttpMethod.GET, "/openvidu/api/sessions/TestSession", HttpURLConnection.HTTP_OK) + .get("connections").getAsJsonObject().get("content").getAsJsonArray().asList().stream() + .map(con -> con.getAsJsonObject().get("connectionId").getAsString()).toArray(String[]::new); + // First user out + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/TestSession/connection/" + connectionIds[0], + HttpURLConnection.HTTP_NO_CONTENT); + for (int i = 0; i < 3; i++) { + Assertions.assertEquals("forceDisconnectByServer", + CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 2).get("reason").getAsString()); + } + Assertions.assertEquals("forceDisconnectByServer", + CustomWebhook.waitForEvent("participantLeft", 2).get("reason").getAsString()); + // Second user out + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/TestSession/connection/" + connectionIds[1], + HttpURLConnection.HTTP_NO_CONTENT); + Assertions.assertEquals("forceDisconnectByServer", + CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 2).get("reason").getAsString()); + Assertions.assertEquals("forceDisconnectByServer", + CustomWebhook.waitForEvent("participantLeft", 2).get("reason").getAsString()); + for (int i = 0; i < 2; i++) { + Assertions.assertEquals("lastParticipantLeft", + CustomWebhook.waitForEvent("recordingStatusChanged", 2).get("reason").getAsString()); + } + Assertions.assertEquals("lastParticipantLeft", + CustomWebhook.waitForEvent("sessionDestroyed", 2).get("reason").getAsString()); + CustomWebhook.events.values().forEach(collection -> Assertions.assertTrue(collection.isEmpty())); + + // sessionClosedByServer: webrtcConnectionDestroyed, participantLeft, + // sessionDestroyed, recordingStatusChanged + this.connectTwoUsers(user, restClient, false, true, true); + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/TestSession", + HttpURLConnection.HTTP_NO_CONTENT); + for (int i = 0; i < 4; i++) { + Assertions.assertEquals("sessionClosedByServer", + CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 2).get("reason").getAsString()); + } + for (int i = 0; i < 2; i++) { + Assertions.assertEquals("sessionClosedByServer", + CustomWebhook.waitForEvent("participantLeft", 2).get("reason").getAsString()); + } + for (int i = 0; i < 2; i++) { + Assertions.assertEquals("sessionClosedByServer", + CustomWebhook.waitForEvent("recordingStatusChanged", 2).get("reason").getAsString()); + } + Assertions.assertEquals("sessionClosedByServer", + CustomWebhook.waitForEvent("sessionDestroyed", 2).get("reason").getAsString()); + CustomWebhook.events.values().forEach(collection -> Assertions.assertTrue(collection.isEmpty())); + + // networkDisconnect: webrtcConnectionDestroyed, participantLeft (and + // subsequent lastParticipantLeft triggered events for sessionDestroyed, + // recordingStatusChanged, [broadcastStopped]) + this.connectTwoUsers(user, restClient, false, true, true); + // First user out + user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 .network-drop-btn")).click(); + for (int i = 0; i < 3; i++) { + Assertions.assertEquals("networkDisconnect", + CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 20).get("reason").getAsString()); + } + Assertions.assertEquals("networkDisconnect", + CustomWebhook.waitForEvent("participantLeft", 2).get("reason").getAsString()); + // Second user out + user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 .network-drop-btn")).click(); + Assertions.assertEquals("networkDisconnect", + CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 20).get("reason").getAsString()); + Assertions.assertEquals("networkDisconnect", + CustomWebhook.waitForEvent("participantLeft", 2).get("reason").getAsString()); + for (int i = 0; i < 2; i++) { + Assertions.assertEquals("lastParticipantLeft", + CustomWebhook.waitForEvent("recordingStatusChanged", 2).get("reason").getAsString()); + } + Assertions.assertEquals("lastParticipantLeft", + CustomWebhook.waitForEvent("sessionDestroyed", 2).get("reason").getAsString()); + CustomWebhook.events.values().forEach(collection -> Assertions.assertTrue(collection.isEmpty())); + + // mediaServerDisconnect: webrtcConnectionDestroyed, participantLeft, + // sessionDestroyed, recordingStatusChanged, [broadcastStopped] + this.connectTwoUsers(user, restClient, false, true, true); + String mediaNodeId = restClient + .rest(HttpMethod.GET, "/openvidu/api/media-nodes", HttpURLConnection.HTTP_OK).get("content") + .getAsJsonArray().get(0).getAsJsonObject().get("id").getAsString(); + restClient.rest(HttpMethod.DELETE, + "/openvidu/api/media-nodes/" + mediaNodeId + "?wait=false&deletion-strategy=now", + HttpURLConnection.HTTP_OK); + CustomWebhook.waitForEvent("mediaNodeStatusChanged", 3); + for (int i = 0; i < 4; i++) { + Assertions.assertEquals("mediaServerDisconnect", + CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 20).get("reason").getAsString()); + } + for (int i = 0; i < 2; i++) { + Assertions.assertEquals("mediaServerDisconnect", + CustomWebhook.waitForEvent("participantLeft", 2).get("reason").getAsString()); + } + for (int i = 0; i < 2; i++) { + Assertions.assertEquals("mediaServerDisconnect", + CustomWebhook.waitForEvent("recordingStatusChanged", 4).get("reason").getAsString()); + } + Assertions.assertEquals("mediaServerDisconnect", + CustomWebhook.waitForEvent("sessionDestroyed", 2).get("reason").getAsString()); + CustomWebhook.waitForEvent("mediaNodeStatusChanged", 5); + CustomWebhook.events.values().forEach(collection -> Assertions.assertTrue(collection.isEmpty())); + restartOpenViduServer(new HashMap<>(), true, HttpURLConnection.HTTP_OK); + + // mediaServerReconnect: webrtcConnectionDestroyed, recordingStatusChanged + this.connectTwoUsers(user, restClient, false, true, true); + String containerId = restClient + .rest(HttpMethod.GET, "/openvidu/api/media-nodes", HttpURLConnection.HTTP_OK).get("content") + .getAsJsonArray().get(0).getAsJsonObject().get("environmentId").getAsString(); + MediaNodeDockerUtils.stopMediaServerInsideMediaNodeAndRecover(containerId, 400); + for (int i = 0; i < 4; i++) { + Assertions.assertEquals("mediaServerReconnect", + CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 2).get("reason").getAsString()); + } + for (int i = 0; i < 2; i++) { + Assertions.assertEquals("mediaServerReconnect", + CustomWebhook.waitForEvent("recordingStatusChanged", 2).get("reason").getAsString()); + } + CustomWebhook.events.values().forEach(collection -> Assertions.assertTrue(collection.isEmpty())); + + // nodeCrashed: webrtcConnectionDestroyed, participantLeft, sessionDestroyed, + // recordingStatusChanged, [broadcastStopped] + this.connectTwoUsers(user, restClient, false, true, true); + containerId = restClient.rest(HttpMethod.GET, "/openvidu/api/media-nodes", HttpURLConnection.HTTP_OK) + .get("content").getAsJsonArray().get(0).getAsJsonObject().get("environmentId").getAsString(); + MediaNodeDockerUtils.crashMediaNode(containerId); + CustomWebhook.waitForEvent("nodeCrashed", 10); + CustomWebhook.waitForEvent("mediaNodeStatusChanged", 2); + for (int i = 0; i < 4; i++) { + Assertions.assertEquals("nodeCrashed", + CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 2).get("reason").getAsString()); + } + for (int i = 0; i < 2; i++) { + Assertions.assertEquals("nodeCrashed", + CustomWebhook.waitForEvent("participantLeft", 2).get("reason").getAsString()); + } + // Only status "stopped" for recording. Not "ready" if node crash + Assertions.assertEquals("nodeCrashed", + CustomWebhook.waitForEvent("recordingStatusChanged", 2).get("reason").getAsString()); + Assertions.assertEquals("nodeCrashed", + CustomWebhook.waitForEvent("sessionDestroyed", 2).get("reason").getAsString()); + CustomWebhook.waitForEvent("mediaNodeStatusChanged", 2); + CustomWebhook.events.values().forEach(collection -> Assertions.assertTrue(collection.isEmpty())); + restartOpenViduServer(new HashMap<>(), true, HttpURLConnection.HTTP_OK); + + // openviduServerStopped: webrtcConnectionDestroyed, participantLeft, + // sessionDestroyed, recordingStatusChanged, [broadcastStopped] + this.connectTwoUsers(user, restClient, false, true, true); + restartOpenViduServer(new HashMap<>(), true, HttpURLConnection.HTTP_OK); + for (int i = 0; i < 4; i++) { + Assertions.assertEquals("openviduServerStopped", + CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 20).get("reason").getAsString()); + } + for (int i = 0; i < 2; i++) { + Assertions.assertEquals("openviduServerStopped", + CustomWebhook.waitForEvent("participantLeft", 2).get("reason").getAsString()); + } + for (int i = 0; i < 2; i++) { + Assertions.assertEquals("openviduServerStopped", + CustomWebhook.waitForEvent("recordingStatusChanged", 4).get("reason").getAsString()); + } + Assertions.assertEquals("openviduServerStopped", + CustomWebhook.waitForEvent("sessionDestroyed", 2).get("reason").getAsString()); + for (int i = 0; i < 2; i++) { + CustomWebhook.waitForEvent("mediaNodeStatusChanged", 15); + } + CustomWebhook.events.values().forEach(collection -> Assertions.assertTrue(collection.isEmpty())); + + // automaticStop: sessionDestroyed, recordingStatusChanged + newConfig = Map.of("OPENVIDU_RECORDING_AUTOSTOP_TIMEOUT", 1); + restartOpenViduServer(newConfig); + this.connectTwoUsers(user, restClient, false, true, true); + user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 .leave-btn")).click(); + user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 .leave-btn")).click(); + for (int i = 0; i < 4; i++) { + Assertions.assertEquals("disconnect", + CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 2).get("reason").getAsString()); + } + for (int i = 0; i < 2; i++) { + Assertions.assertEquals("disconnect", + CustomWebhook.waitForEvent("participantLeft", 2).get("reason").getAsString()); + } + for (int i = 0; i < 2; i++) { + Assertions.assertEquals("automaticStop", + CustomWebhook.waitForEvent("recordingStatusChanged", 4).get("reason").getAsString()); + } + Assertions.assertEquals("automaticStop", + CustomWebhook.waitForEvent("sessionDestroyed", 2).get("reason").getAsString()); + CustomWebhook.events.values().forEach(collection -> Assertions.assertTrue(collection.isEmpty())); + + // recordingStoppedByServer + this.connectTwoUsers(user, restClient, false, true, true); + String recordingId = restClient + .rest(HttpMethod.GET, "/openvidu/api/recordings", HttpURLConnection.HTTP_OK).get("items") + .getAsJsonArray().asList().stream() + .filter(rec -> rec.getAsJsonObject().get("status").getAsString() + .equals(Recording.Status.started.name())) + .findFirst().get().getAsJsonObject().get("id").getAsString(); + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/stop/" + recordingId, + HttpURLConnection.HTTP_OK); + for (int i = 0; i < 2; i++) { + Assertions.assertEquals("recordingStoppedByServer", + CustomWebhook.waitForEvent("recordingStatusChanged", 4).get("reason").getAsString()); + } + + // [broadcastStoppedByServer] + + } finally { + Map oldConfig = new HashMap<>(); + oldConfig.put("OPENVIDU_WEBHOOK", false); + if (defaultOpenViduWebhookEndpoint != null) { + oldConfig.put("OPENVIDU_WEBHOOK_ENDPOINT", defaultOpenViduWebhookEndpoint); + } + if (defaultOpenViduRecordingAutostopTimeout != null) { + oldConfig.put("OPENVIDU_RECORDING_AUTOSTOP_TIMEOUT", defaultOpenViduRecordingAutostopTimeout); + } + restartOpenViduServer(oldConfig); + } + + } finally { + CustomWebhook.shutDown(); + } + } + @Test @DisplayName("Individual dynamic record") void individualDynamicRecordTest() throws Exception { @@ -2552,4 +2903,40 @@ public class OpenViduProTestAppE2eTest extends AbstractOpenViduTestappE2eTest { sttUnsubUser(user, 0, 0, false, true); } + private void connectTwoUsers(OpenViduTestappUser user, CustomHttpClient restClient, boolean firstUserIsModerator, + boolean startRecording, boolean startBroadcast) throws Exception { + this.closeAllSessions(OV); + user.getDriver().findElement(By.id("remove-all-users-btn")).click(); + user.getEventManager().clearAllCurrentEvents(); + CustomWebhook.clean(); + user.getDriver().findElement(By.id("add-user-btn")).click(); + if (firstUserIsModerator) { + user.getDriver().findElement(By.id("session-settings-btn-0")).click(); + Thread.sleep(500); + user.getDriver().findElement(By.id("radio-btn-mod")).click(); + user.getDriver().findElement(By.id("save-btn")).click(); + Thread.sleep(500); + } + user.getDriver().findElement(By.id("add-user-btn")).click(); + user.getDriver().findElements(By.className("join-btn")).forEach(el -> el.sendKeys(Keys.ENTER)); + user.getEventManager().waitUntilEventReaches("streamPlaying", 4); + CustomWebhook.waitForEvent("sessionCreated", 1); + for (int i = 0; i < 2; i++) { + CustomWebhook.waitForEvent("participantJoined", 1); + } + for (int i = 0; i < 4; i++) { + CustomWebhook.waitForEvent("webrtcConnectionCreated", 1); + } + if (startRecording) { + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", + "{'session':'TestSession','outputMode':'INDIVIDUAL'}", HttpURLConnection.HTTP_OK); + user.getEventManager().waitUntilEventReaches("recordingStarted", 2); + Assertions.assertEquals(Recording.Status.started.name(), + CustomWebhook.waitForEvent("recordingStatusChanged", 3).get("status").getAsString()); + } + if (startBroadcast) { + + } + } + } diff --git a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.html b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.html index e24b5d77..b731f99b 100644 --- a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.html +++ b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.html @@ -124,6 +124,9 @@ + diff --git a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.ts b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.ts index a531cfce..91466ef3 100644 --- a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.ts +++ b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.ts @@ -284,6 +284,11 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { this.subscribers = []; } + private simulateNetworkDrop(): void { + const jsonRpClient = (this.OV as any).jsonRpcClient; + jsonRpClient.close(); + } + updateEventList(eventName: string, eventContent: string, event: Event) { const eventInterface: OpenViduEvent = { eventName, eventContent, event }; this.events.push(eventInterface);