openvidu-server: webhook

pull/375/head
pabloFuente 2019-06-24 15:15:28 +02:00
parent d72063b97d
commit 9c4941de9a
22 changed files with 530 additions and 54 deletions

View File

@ -19,7 +19,8 @@ package io.openvidu.server;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
@ -39,6 +40,7 @@ import org.springframework.context.event.EventListener;
import io.openvidu.client.OpenViduException;
import io.openvidu.client.OpenViduException.Code;
import io.openvidu.server.cdr.CDRLogger;
import io.openvidu.server.cdr.CDRLoggerFile;
import io.openvidu.server.cdr.CallDetailRecord;
import io.openvidu.server.config.HttpHandshakeInterceptor;
@ -64,6 +66,7 @@ import io.openvidu.server.rpc.RpcNotificationService;
import io.openvidu.server.utils.CommandExecutor;
import io.openvidu.server.utils.GeoLocationByIp;
import io.openvidu.server.utils.GeoLocationByIpDummy;
import io.openvidu.server.webhook.CDRLoggerWebhook;
/**
* OpenVidu Server application
@ -129,10 +132,15 @@ public class OpenViduServer implements JsonRpcConfigurer {
@Bean
@ConditionalOnMissingBean
public CallDetailRecord cdr() {
if (this.openviduConfig.isCdrEnabled()) {
List<CDRLogger> loggers = new ArrayList<>();
if (openviduConfig.isCdrEnabled()) {
log.info("OpenVidu CDR is enabled");
loggers.add(new CDRLoggerFile());
}
return new CallDetailRecord(Arrays.asList(new CDRLoggerFile()));
if (openviduConfig.isWebhookEnabled()) {
loggers.add(new CDRLoggerWebhook(openviduConfig));
}
return new CallDetailRecord(loggers);
}
@Bean

View File

@ -23,7 +23,7 @@ public class CDREvent {
protected String sessionId;
protected Long timeStamp;
private CDREventName eventName;
protected CDREventName eventName;
public CDREvent(CDREventName eventName, String sessionId, Long timeStamp) {
this.eventName = eventName;

View File

@ -33,8 +33,8 @@ public class CDREventParticipant extends CDREventEnd {
}
// participantLeft
public CDREventParticipant(CDREventParticipant event, EndReason reason) {
super(CDREventName.participantLeft, event.getSessionId(), event.getTimestamp(), reason);
public CDREventParticipant(CDREventParticipant event, EndReason reason, Long timestamp) {
super(CDREventName.participantLeft, event.getSessionId(), event.getTimestamp(), reason, timestamp);
this.participant = event.participant;
}

View File

@ -25,7 +25,7 @@ import io.openvidu.server.recording.Recording;
public class CDREventRecording extends CDREventEnd {
private Recording recording;
protected Recording recording;
// recordingStarted
public CDREventRecording(String sessionId, Recording recording) {
@ -34,9 +34,9 @@ public class CDREventRecording extends CDREventEnd {
}
// recordingStopped
public CDREventRecording(CDREventRecording event, Recording recording, EndReason reason) {
public CDREventRecording(CDREventRecording event, Recording recording, EndReason reason, Long timestamp) {
super(CDREventName.recordingStopped, event == null ? recording.getSessionId() : event.getSessionId(),
event == null ? recording.getCreatedAt() : event.getTimestamp(), reason);
event == null ? recording.getCreatedAt() : event.getTimestamp(), reason, timestamp);
this.recording = recording;
}

View File

@ -0,0 +1,54 @@
/*
* (C) Copyright 2017-2019 OpenVidu (https://openvidu.io/)
*
* 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.
*
*/
package io.openvidu.server.cdr;
import com.google.gson.JsonObject;
import io.openvidu.java.client.Recording.Status;
import io.openvidu.server.core.EndReason;
import io.openvidu.server.recording.Recording;
public class CDREventRecordingStatus extends CDREventRecording {
private Status status;
public CDREventRecordingStatus(String sessionId, Recording recording, Status status) {
super(sessionId, recording);
this.eventName = CDREventName.recordingStatusChanged;
this.status = status;
}
public CDREventRecordingStatus(CDREventRecording recordingStartedEvent, Recording recording, EndReason finalReason,
long timestamp, Status status) {
super(recordingStartedEvent, recording, finalReason, timestamp);
this.eventName = CDREventName.recordingStatusChanged;
this.status = status;
}
public Status getStatus() {
return status;
}
@Override
public JsonObject toJson() {
JsonObject json = super.toJson();
json.addProperty("status", this.status.name());
return json;
}
}

View File

@ -31,8 +31,8 @@ public class CDREventSession extends CDREventEnd {
}
// sessionDestroyed
public CDREventSession(CDREventSession event, EndReason reason) {
super(CDREventName.sessionDestroyed, event.getSessionId(), event.getTimestamp(), reason);
public CDREventSession(CDREventSession event, EndReason reason, Long timestamp) {
super(CDREventName.sessionDestroyed, event.getSessionId(), event.getTimestamp(), reason, timestamp);
this.session = event.session;
}

View File

@ -41,8 +41,8 @@ public class CDREventWebrtcConnection extends CDREventEnd implements Comparable<
}
// webrtcConnectionDestroyed
public CDREventWebrtcConnection(CDREventWebrtcConnection event, EndReason reason) {
super(CDREventName.webrtcConnectionDestroyed, event.getSessionId(), event.getTimestamp(), reason);
public CDREventWebrtcConnection(CDREventWebrtcConnection event, EndReason reason, Long timestamp) {
super(CDREventName.webrtcConnectionDestroyed, event.getSessionId(), event.getTimestamp(), reason, timestamp);
this.streamId = event.streamId;
this.participant = event.participant;
this.mediaOptions = event.mediaOptions;

View File

@ -28,6 +28,4 @@ public interface CDRLogger {
public void log(SessionSummary sessionSummary);
public boolean canBeDisabled();
}

View File

@ -40,9 +40,4 @@ public class CDRLoggerFile implements CDRLogger {
public void log(SessionSummary sessionSummary) {
}
@Override
public boolean canBeDisabled() {
return true;
}
}

View File

@ -26,6 +26,7 @@ import java.util.concurrent.ConcurrentSkipListSet;
import org.springframework.beans.factory.annotation.Autowired;
import io.openvidu.java.client.Recording.Status;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.core.EndReason;
import io.openvidu.server.core.MediaOptions;
@ -49,6 +50,7 @@ import io.openvidu.server.summary.SessionSummary;
* - 'webrtcConnectionDestroyed' {sessionId, timestamp, participantId, startTime, duration, connection, [receivingFrom], audioEnabled, videoEnabled, [videoSource], [videoFramerate], reason}
* - 'recordingStarted' {sessionId, timestamp, id, name, hasAudio, hasVideo, resolution, recordingLayout, size}
* - 'recordingStopped' {sessionId, timestamp, id, name, hasAudio, hasVideo, resolution, recordingLayout, size}
* - 'recordingStatusChanged' {sessionId, timestamp, id, name, hasAudio, hasVideo, resolution, recordingLayout, size, status}
*
* PROPERTIES VALUES:
*
@ -71,6 +73,7 @@ import io.openvidu.server.summary.SessionSummary;
* - resolution string
* - recordingLayout: string
* - size: number
* - status: string
* - webrtcConnectionDestroyed.reason: "unsubscribe", "unpublish", "disconnect", "networkDisconnect", "mediaServerDisconnect", "openviduServerStopped"
* - participantLeft.reason: "unsubscribe", "unpublish", "disconnect", "networkDisconnect", "openviduServerStopped"
* - sessionDestroyed.reason: "lastParticipantLeft", "openviduServerStopped"
@ -117,7 +120,8 @@ public class CallDetailRecord {
public void recordSessionDestroyed(String sessionId, EndReason reason) {
CDREventSession e = this.sessions.remove(sessionId);
if (e != null) {
CDREventSession eventSessionEnd = new CDREventSession(e, RecordingManager.finalReason(reason));
CDREventSession eventSessionEnd = new CDREventSession(e, RecordingManager.finalReason(reason),
System.currentTimeMillis());
this.log(eventSessionEnd);
// Summary: log closed session
@ -134,7 +138,7 @@ public class CallDetailRecord {
public void recordParticipantLeft(Participant participant, String sessionId, EndReason reason) {
CDREventParticipant e = this.participants.remove(participant.getParticipantPublicId());
CDREventParticipant eventParticipantEnd = new CDREventParticipant(e, reason);
CDREventParticipant eventParticipantEnd = new CDREventParticipant(e, reason, System.currentTimeMillis());
this.log(eventParticipantEnd);
// Summary: update final user ended connection
@ -152,7 +156,7 @@ public class CallDetailRecord {
public void stopPublisher(String participantPublicId, String streamId, EndReason reason) {
CDREventWebrtcConnection eventPublisherEnd = this.publications.remove(participantPublicId);
if (eventPublisherEnd != null) {
eventPublisherEnd = new CDREventWebrtcConnection(eventPublisherEnd, reason);
eventPublisherEnd = new CDREventWebrtcConnection(eventPublisherEnd, reason, System.currentTimeMillis());
this.log(eventPublisherEnd);
// Summary: update final user ended publisher
@ -180,7 +184,8 @@ public class CallDetailRecord {
eventSubscriberEnd = it.next();
if (senderPublicId.equals(eventSubscriberEnd.receivingFrom)) {
it.remove();
eventSubscriberEnd = new CDREventWebrtcConnection(eventSubscriberEnd, reason);
eventSubscriberEnd = new CDREventWebrtcConnection(eventSubscriberEnd, reason,
System.currentTimeMillis());
this.log(eventSubscriberEnd);
// Summary: update final user ended subscriber
@ -196,23 +201,35 @@ public class CallDetailRecord {
CDREventRecording recordingStartedEvent = new CDREventRecording(sessionId, recording);
this.recordings.putIfAbsent(recording.getId(), recordingStartedEvent);
this.log(new CDREventRecording(sessionId, recording));
this.recordRecordingStatusChanged(sessionId, recording, Status.started);
}
public void recordRecordingStopped(String sessionId, Recording recording, EndReason reason) {
CDREventRecording recordingStartedEvent = this.recordings.remove(recording.getId());
final long timestamp = System.currentTimeMillis();
CDREventRecording recordingStoppedEvent = new CDREventRecording(recordingStartedEvent, recording,
RecordingManager.finalReason(reason));
RecordingManager.finalReason(reason), timestamp);
this.log(recordingStoppedEvent);
this.recordRecordingStatusChanged(recordingStartedEvent, recording, RecordingManager.finalReason(reason),
timestamp, Status.stopped);
// Summary: update ended recording
sessionManager.getAccumulatedRecordings(sessionId).add(recordingStoppedEvent);
}
public void recordRecordingStatusChanged(String sessionId, Recording recording, Status status) {
this.log(new CDREventRecordingStatus(sessionId, recording, status));
}
public void recordRecordingStatusChanged(CDREventRecording recordingStartedEvent, Recording recording,
EndReason finalReason, long timestamp, Status status) {
this.log(new CDREventRecordingStatus(recordingStartedEvent, recording, finalReason, timestamp, status));
}
private void log(CDREvent event) {
this.loggers.forEach(logger -> {
if (openviduConfig.isCdrEnabled() || !logger.canBeDisabled()) {
logger.log(event);
}
logger.log(event);
});
}

View File

@ -37,9 +37,11 @@ import com.github.dockerjava.api.model.Volume;
import io.openvidu.client.OpenViduException;
import io.openvidu.client.OpenViduException.Code;
import io.openvidu.java.client.Recording.Status;
import io.openvidu.java.client.RecordingLayout;
import io.openvidu.java.client.RecordingProperties;
import io.openvidu.server.OpenViduServer;
import io.openvidu.server.cdr.CallDetailRecord;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.core.EndReason;
import io.openvidu.server.core.Participant;
@ -63,8 +65,8 @@ public class ComposedRecordingService extends RecordingService {
private DockerManager dockerManager;
public ComposedRecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader,
OpenviduConfig openviduConfig) {
super(recordingManager, recordingDownloader, openviduConfig);
OpenviduConfig openviduConfig, CallDetailRecord cdr) {
super(recordingManager, recordingDownloader, openviduConfig, cdr);
this.dockerManager = new DockerManager();
}
@ -97,6 +99,7 @@ public class ComposedRecordingService extends RecordingService {
return this.stopRecordingWithVideo(session, recording, reason);
} else {
recording = this.sealRecordingMetadataFileAsProcessing(recording);
this.cdr.recordRecordingStatusChanged(session.getSessionId(), recording, Status.processing);
return this.stopRecordingAudioOnly(session, recording, reason, 0);
}
}

View File

@ -56,6 +56,7 @@ import io.openvidu.client.OpenViduException.Code;
import io.openvidu.client.internal.ProtocolElements;
import io.openvidu.java.client.Recording.OutputMode;
import io.openvidu.java.client.RecordingProperties;
import io.openvidu.server.cdr.CallDetailRecord;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.core.EndReason;
import io.openvidu.server.core.Participant;
@ -91,6 +92,9 @@ public class RecordingManager {
@Autowired
private KmsManager kmsManager;
@Autowired
private CallDetailRecord cdr;
protected Map<String, Recording> startingRecordings = new ConcurrentHashMap<>();
protected Map<String, Recording> startedRecordings = new ConcurrentHashMap<>();
@ -113,8 +117,8 @@ public class RecordingManager {
RecordingManager.IMAGE_TAG = openviduConfig.getOpenViduRecordingVersion();
this.dockerManager = new DockerManager();
this.composedRecordingService = new ComposedRecordingService(this, recordingDownloader, openviduConfig);
this.singleStreamRecordingService = new SingleStreamRecordingService(this, recordingDownloader, openviduConfig);
this.composedRecordingService = new ComposedRecordingService(this, recordingDownloader, openviduConfig, cdr);
this.singleStreamRecordingService = new SingleStreamRecordingService(this, recordingDownloader, openviduConfig, cdr);
log.info("Recording module required: Downloading openvidu/openvidu-recording:"
+ openviduConfig.getOpenViduRecordingVersion() + " Docker image (350MB aprox)");

View File

@ -26,6 +26,7 @@ import io.openvidu.client.OpenViduException;
import io.openvidu.client.OpenViduException.Code;
import io.openvidu.java.client.RecordingLayout;
import io.openvidu.java.client.RecordingProperties;
import io.openvidu.server.cdr.CallDetailRecord;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.core.EndReason;
import io.openvidu.server.core.Session;
@ -41,13 +42,15 @@ public abstract class RecordingService {
protected OpenviduConfig openviduConfig;
protected RecordingManager recordingManager;
protected RecordingDownloader recordingDownloader;
protected CallDetailRecord cdr;
protected CustomFileManager fileWriter = new CustomFileManager();
RecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader,
OpenviduConfig openviduConfig) {
OpenviduConfig openviduConfig, CallDetailRecord cdr) {
this.recordingManager = recordingManager;
this.recordingDownloader = recordingDownloader;
this.openviduConfig = openviduConfig;
this.cdr = cdr;
}
public abstract Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException;

View File

@ -52,7 +52,9 @@ import com.google.gson.JsonObject;
import io.openvidu.client.OpenViduException;
import io.openvidu.client.OpenViduException.Code;
import io.openvidu.java.client.Recording.Status;
import io.openvidu.java.client.RecordingProperties;
import io.openvidu.server.cdr.CallDetailRecord;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.core.EndReason;
import io.openvidu.server.core.Participant;
@ -71,8 +73,8 @@ public class SingleStreamRecordingService extends RecordingService {
private final String INDIVIDUAL_STREAM_METADATA_FILE = ".stream.";
public SingleStreamRecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader,
OpenviduConfig openviduConfig) {
super(recordingManager, recordingDownloader, openviduConfig);
OpenviduConfig openviduConfig, CallDetailRecord cdr) {
super(recordingManager, recordingDownloader, openviduConfig, cdr);
}
@Override
@ -131,6 +133,7 @@ public class SingleStreamRecordingService extends RecordingService {
@Override
public Recording stopRecording(Session session, Recording recording, EndReason reason) {
recording = this.sealRecordingMetadataFileAsProcessing(recording);
this.cdr.recordRecordingStatusChanged(session.getSessionId(), recording, Status.processing);
return this.stopRecording(session, recording, reason, 0);
}

View File

@ -0,0 +1,59 @@
/*
* (C) Copyright 2017-2019 OpenVidu (https://openvidu.io/)
*
* 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.
*
*/
package io.openvidu.server.webhook;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.openvidu.server.cdr.CDREvent;
import io.openvidu.server.cdr.CDRLogger;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.kurento.endpoint.KmsEvent;
import io.openvidu.server.summary.SessionSummary;
public class CDRLoggerWebhook implements CDRLogger {
private Logger log = LoggerFactory.getLogger(CDRLoggerWebhook.class);
private HttpWebhookSender webhookSender;
public CDRLoggerWebhook(OpenviduConfig openviduConfig) {
this.webhookSender = new HttpWebhookSender(openviduConfig.getOpenViduWebhookEndpoint(),
openviduConfig.getOpenViduWebhookHeaders(), openviduConfig.getOpenViduWebhookEvents());
}
@Override
public void log(CDREvent event) {
try {
this.webhookSender.sendHttpPostCallback(event);
} catch (IOException e) {
log.error("Error sending webhook event: {}", e.getMessage());
}
}
@Override
public void log(KmsEvent event) {
}
@Override
public void log(SessionSummary sessionSummary) {
}
}

View File

@ -0,0 +1,160 @@
/*
* (C) Copyright 2017-2019 OpenVidu (https://openvidu.io/)
*
* 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.
*
*/
package io.openvidu.server.webhook;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import org.apache.http.Header;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicHeader;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.TrustStrategy;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonObject;
import io.openvidu.server.cdr.CDREvent;
import io.openvidu.server.cdr.CDREventName;
public class HttpWebhookSender {
private static final Logger log = LoggerFactory.getLogger(HttpWebhookSender.class);
private HttpClient httpClient;
private String httpEndpoint;
private List<Header> headers;
private List<CDREventName> events;
public HttpWebhookSender(String httpEndpoint, List<Header> headers, List<CDREventName> events) {
this.httpEndpoint = httpEndpoint;
this.headers = headers;
boolean contentTypeHeaderAdded = false;
for (Header header : this.headers) {
if (HttpHeaders.CONTENT_TYPE.equals(header.getName()) && "application/json".equals(header.getValue())) {
contentTypeHeaderAdded = true;
break;
}
}
if (!contentTypeHeaderAdded) {
headers.add(new BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json"));
}
this.events = events;
TrustStrategy trustStrategy = new TrustStrategy() {
@Override
public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
return true;
}
};
SSLContext sslContext;
try {
sslContext = new SSLContextBuilder().loadTrustMaterial(null, trustStrategy).build();
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
throw new RuntimeException(e);
}
RequestConfig.Builder requestBuilder = RequestConfig.custom();
requestBuilder = requestBuilder.setConnectTimeout(30000);
requestBuilder = requestBuilder.setConnectionRequestTimeout(30000);
this.httpClient = HttpClientBuilder.create().setDefaultRequestConfig(requestBuilder.build())
.setConnectionTimeToLive(30, TimeUnit.SECONDS).setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
.setSSLContext(sslContext).build();
}
/**
* @throws IOException If: A) The HTTP connection cannot be established to the
* endpoint B) The response received from the endpoint is
* not 200
*/
public void sendHttpPostCallback(CDREvent event) throws IOException {
if (!this.events.contains(event.getEventName())) {
return;
}
HttpPost request = new HttpPost(httpEndpoint);
StringEntity params = null;
try {
JsonObject jsonEvent = event.toJson();
jsonEvent.addProperty("event", event.getEventName().name());
params = new StringEntity(jsonEvent.toString());
} catch (UnsupportedEncodingException e) {
log.error("Cannot create StringEntity from JSON CDREvent. Default HTTP charset is not supported");
}
for (Header header : this.headers) {
request.setHeader(header);
}
request.setEntity(params);
HttpResponse response = null;
try {
response = this.httpClient.execute(request);
int statusCode = response.getStatusLine().getStatusCode();
if ((statusCode == org.apache.http.HttpStatus.SC_OK)) {
log.info("Event {} successfully posted to uri {}", event.getEventName().name(), this.httpEndpoint);
} else {
log.error("Unexpected HTTP status from callback endpoint {}: expected 200, received {}", httpEndpoint,
statusCode);
throw new IOException("Unexpected HTTP status " + statusCode);
}
} catch (ClientProtocolException e) {
String message = "ClientProtocolException posting event [" + event.getEventName().name() + "] to endpoint "
+ httpEndpoint + ": " + e.getMessage();
log.error(message);
throw new ClientProtocolException(message);
} catch (IOException e) {
String message = "IOException posting event [" + event.getEventName().name() + "] to endpoint "
+ httpEndpoint + ": " + e.getMessage();
log.error(message);
throw new IOException(message);
} finally {
if (response != null) {
EntityUtils.consumeQuietly(response.getEntity());
}
}
}
}

View File

@ -17,10 +17,10 @@ openvidu.secret: MY_SECRET
openvidu.cdr: false
openvidu.cdr.path: log
openvidu.webhook: true
openvidu.webhook.endpoint: https://localhost/openvidu-endpoint
openvidu.webhook.headers: [\"Authorization:\ Basic\ T1BFTlZJRFVBUFA6TVlfU0VDUkVU\"]
openvidu.webhook.events: [\"sessionCreated\",\"sessionDestroyed\",\"recordingStatusChanged\"]
openvidu.webhook: false
openvidu.webhook.endpoint:
openvidu.webhook.headers:
openvidu.webhook.events: [\"sessionCreated\",\"sessionDestroyed\",\"participantJoined\",\"participantLeft\",\"webrtcConnectionCreated\",\"webrtcConnectionDestroyed\",\"recordingStatusChanged\"]
openvidu.recording: false
openvidu.recording.version: 2.9.0

View File

@ -57,10 +57,10 @@ node('container') {
sh(script: '''#!/bin/bash
if [ "$DOCKER_RECORDING_VERSION" != "default" ]; then
echo "Using custom openvidu-recording tag: $DOCKER_RECORDING_VERSION"
cd openvidu/openvidu-server/target && java -jar -Dopenvidu.publicurl=https://172.17.0.1:4443/ -Dopenvidu.recording=true -Dopenvidu.recording.version=$DOCKER_RECORDING_VERSION openvidu-server-*.jar &> /openvidu-server.log &
cd openvidu/openvidu-server/target && java -jar -Dopenvidu.publicurl=https://172.17.0.1:4443/ -Dopenvidu.recording=true -Dopenvidu.recording.version=$DOCKER_RECORDING_VERSION -Dopenvidu.webhook=true -Dopenvidu.webhook.endpoint=http://172.17.0.1:7777/webhook/ openvidu-server-*.jar &> /openvidu-server.log &
else
echo "Using default openvidu-recording tag"
cd openvidu/openvidu-server/target && java -jar -Dopenvidu.publicurl=https://172.17.0.1:4443/ -Dopenvidu.recording=true openvidu-server-*.jar &> /openvidu-server.log &
cd openvidu/openvidu-server/target && java -jar -Dopenvidu.publicurl=https://172.17.0.1:4443/ -Dopenvidu.recording=true -Dopenvidu.webhook=true -Dopenvidu.webhook.endpoint=http://172.17.0.1:7777/webhook/ openvidu-server-*.jar &> /openvidu-server.log &
fi
'''.stripIndent())
sh 'until $(curl --insecure --output /dev/null --silent --head --fail https://OPENVIDUAPP:MY_SECRET@localhost:4443/); do echo "Waiting for openvidu-server..."; sleep 2; done'

View File

@ -164,6 +164,11 @@
<version>${version.openvidu.java.client}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${version.spring-boot}</version>
</dependency>
</dependencies>
</project>

View File

@ -102,6 +102,7 @@ import io.openvidu.test.browsers.FirefoxUser;
import io.openvidu.test.browsers.OperaUser;
import io.openvidu.test.e2e.utils.CommandLineExecutor;
import io.openvidu.test.e2e.utils.CustomHttpClient;
import io.openvidu.test.e2e.utils.CustomWebhook;
import io.openvidu.test.e2e.utils.MultimediaFileMetadata;
import io.openvidu.test.e2e.utils.Unzipper;
@ -2725,6 +2726,70 @@ public class OpenViduTestAppE2eTest {
gracefullyLeaveParticipants(2);
}
@Test
@DisplayName("Webhook test")
void webhookTest() throws Exception {
isRecordingTest = true;
setupBrowser("chrome");
log.info("Webhook test");
CountDownLatch initLatch = new CountDownLatch(1);
CustomWebhook.main(new String[0], initLatch);
try {
if (!initLatch.await(30, TimeUnit.SECONDS)) {
Assert.fail("Tiemout waiting for webhook springboot app to start");
CustomWebhook.shutDown();
return;
}
user.getDriver().findElement(By.id("add-user-btn")).click();
user.getDriver().findElement(By.id("session-settings-btn-0")).click();
Thread.sleep(1000);
user.getDriver().findElement(By.id("recording-mode-select")).click();
Thread.sleep(500);
user.getDriver().findElement(By.id("option-ALWAYS")).click();
Thread.sleep(500);
user.getDriver().findElement(By.id("output-mode-select")).click();
Thread.sleep(500);
user.getDriver().findElement(By.id("option-INDIVIDUAL")).click();
Thread.sleep(500);
user.getDriver().findElement(By.id("save-btn")).click();
Thread.sleep(1000);
user.getDriver().findElement(By.className("join-btn")).click();
CustomWebhook.waitForEvent("sessionCreated", 2);
CustomWebhook.waitForEvent("participantJoined", 2);
CustomWebhook.waitForEvent("webrtcConnectionCreated", 2);
JsonObject event = CustomWebhook.waitForEvent("recordingStatusChanged", 10);
Assert.assertEquals("Wrong recording status in webhook event", "started", event.get("status").getAsString());
user.getDriver().findElement(By.id("session-api-btn-0")).click();
Thread.sleep(1000);
user.getDriver().findElement(By.id("close-session-btn")).click();
user.getDriver().findElement(By.id("close-dialog-btn")).click();
Thread.sleep(1000);
CustomWebhook.waitForEvent("webrtcConnectionDestroyed", 2);
CustomWebhook.waitForEvent("participantLeft", 2);
event = CustomWebhook.waitForEvent("recordingStatusChanged", 2);
Assert.assertEquals("Wrong recording status in webhook event", "processing", event.get("status").getAsString());
event = CustomWebhook.waitForEvent("recordingStatusChanged", 2);
Assert.assertEquals("Wrong recording status in webhook event", "stopped", event.get("status").getAsString());
CustomWebhook.waitForEvent("sessionDestroyed", 2);
} finally {
CustomWebhook.shutDown();
}
}
private void listEmptyRecordings() {
// List existing recordings (empty)
user.getDriver().findElement(By.id("list-recording-btn")).click();

View File

@ -0,0 +1,91 @@
/*
* (C) Copyright 2017-2019 OpenVidu (https://openvidu.io/)
*
* 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.
*
*/
package io.openvidu.test.e2e.utils;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
@SpringBootApplication
public class CustomWebhook {
private static ConfigurableApplicationContext context;
public static CountDownLatch initLatch;
private static Map<String, BlockingQueue<JsonObject>> events = new ConcurrentHashMap<>();
private static JsonParser jsonParser = new JsonParser();
public static void main(String[] args, CountDownLatch initLatch) {
CustomWebhook.initLatch = initLatch;
SpringApplication app = new SpringApplication(CustomWebhook.class);
app.setDefaultProperties(Collections.singletonMap("server.port", "7777"));
CustomWebhook.context = app.run(args);
}
public static void shutDown() {
CustomWebhook.context.close();
}
public static JsonObject waitForEvent(String eventName, int maxSecondsWait) throws Exception {
if (events.get(eventName) == null) {
events.put(eventName, new LinkedBlockingDeque<>());
}
JsonObject event = CustomWebhook.events.get(eventName).poll(maxSecondsWait, TimeUnit.SECONDS);
if (event == null) {
throw new Exception("Timeout waiting for Webhook " + eventName);
} else {
return event;
}
}
@RestController
public class GreetingController {
@RequestMapping("/webhook")
public void greeting(@RequestBody String eventString) {
JsonObject event = (JsonObject) jsonParser.parse(eventString);
final String eventName = event.get("event").getAsString();
System.out.println(event.toString());
if (events.get(eventName) == null) {
events.put(eventName, new LinkedBlockingDeque<>());
}
CustomWebhook.events.get(eventName).add(event);
}
}
@EventListener(ApplicationReadyEvent.class)
public void doSomethingAfterStartup() {
CustomWebhook.initLatch.countDown();
}
}

View File

@ -11,16 +11,18 @@
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-select placeholder="RecordingMode" [(ngModel)]="sessionProperties.recordingMode">
<mat-select placeholder="RecordingMode" [(ngModel)]="sessionProperties.recordingMode"
id="recording-mode-select">
<mat-option *ngFor="let enumerator of enumToArray(recordingMode)" [value]="enumerator">
{{ enumerator }}
<span [attr.id]="'option-' + enumerator">{{ enumerator }}</span>
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-select placeholder="DefaultOutputMode" [(ngModel)]="sessionProperties.defaultOutputMode">
<mat-select placeholder="DefaultOutputMode" [(ngModel)]="sessionProperties.defaultOutputMode"
id="output-mode-select">
<mat-option *ngFor="let enumerator of enumToArray(defaultOutputMode)" [value]="enumerator">
{{ enumerator }}
<span [attr.id]="'option-' + enumerator">{{ enumerator }}</span>
</mat-option>
</mat-select>
</mat-form-field>
@ -31,8 +33,10 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field *ngIf="this.sessionProperties.defaultOutputMode === 'COMPOSED' && this.sessionProperties.defaultRecordingLayout === 'CUSTOM'">
<input matInput placeholder="DefaultCustomLayout" type="text" [(ngModel)]="sessionProperties.defaultCustomLayout">
<mat-form-field
*ngIf="this.sessionProperties.defaultOutputMode === 'COMPOSED' && this.sessionProperties.defaultRecordingLayout === 'CUSTOM'">
<input matInput placeholder="DefaultCustomLayout" type="text"
[(ngModel)]="sessionProperties.defaultCustomLayout">
</mat-form-field>
<mat-form-field>
<input matInput placeholder="CustomSessionId" type="text" [(ngModel)]="sessionProperties.customSessionId">
@ -73,26 +77,32 @@
<label class="label" style="margin-bottom: 8px">Kurento config</label>
<div id="kurento-config-div">
<mat-form-field style="width: 39%; margin-right: 5px">
<input matInput placeholder="Max recv" type="number" [(ngModel)]="tokenOptions.kurentoOptions.videoMaxRecvBandwidth">
<input matInput placeholder="Max recv" type="number"
[(ngModel)]="tokenOptions.kurentoOptions.videoMaxRecvBandwidth">
</mat-form-field>
<mat-form-field style="width: 39%">
<input matInput placeholder="Min recv" type="number" [(ngModel)]="tokenOptions.kurentoOptions.videoMinRecvBandwidth">
<input matInput placeholder="Min recv" type="number"
[(ngModel)]="tokenOptions.kurentoOptions.videoMinRecvBandwidth">
</mat-form-field>
<mat-form-field style="width: 39%; margin-right: 5px">
<input matInput placeholder="Max send" type="number" [(ngModel)]="tokenOptions.kurentoOptions.videoMaxSendBandwidth">
<input matInput placeholder="Max send" type="number"
[(ngModel)]="tokenOptions.kurentoOptions.videoMaxSendBandwidth">
</mat-form-field>
<mat-form-field style="width: 39%">
<input matInput placeholder="Min send" type="number" [(ngModel)]="tokenOptions.kurentoOptions.videoMinSendBandwidth">
<input matInput placeholder="Min send" type="number"
[(ngModel)]="tokenOptions.kurentoOptions.videoMinSendBandwidth">
</mat-form-field>
<mat-chip-list *ngIf="filters.length > 0">
<mat-chip style="height: 20px" *ngFor="let filterName of filters" (click)="filters.splice(filters.indexOf(filterName, 1))">{{filterName}}</mat-chip>
<mat-chip style="height: 20px" *ngFor="let filterName of filters"
(click)="filters.splice(filters.indexOf(filterName, 1))">{{filterName}}</mat-chip>
</mat-chip-list>
<mat-form-field style="width: 70%">
<input matInput placeholder="Allowed filter" id="allowed-filter-input" type="text" [(ngModel)]="filterName">
</mat-form-field>
<button id="add-allowed-filter-btn" mat-icon-button style="width: 24px; height: 24px; line-height: 24px;"
title="Add allowed filter" (click)="filters.push(filterName); filterName = '';">
<mat-icon style="font-size: 18px; line-height: 18px; width: 18px; height: 18px" aria-label="Add allowed filter">add_circle</mat-icon>
<mat-icon style="font-size: 18px; line-height: 18px; width: 18px; height: 18px"
aria-label="Add allowed filter">add_circle</mat-icon>
</button>
</div>
@ -108,6 +118,7 @@
<mat-dialog-actions>
<button id="cancel-btn" mat-button [mat-dialog-close]="undefined">CANCEL</button>
<button id="save-btn" mat-button [mat-dialog-close]="{sessionProperties: sessionProperties, turnConf: turnConf, manualTurnConf: manualTurnConf, tokenOptions: generateTokenOptions(), customToken: customToken}">SAVE</button>
<button id="save-btn" mat-button
[mat-dialog-close]="{sessionProperties: sessionProperties, turnConf: turnConf, manualTurnConf: manualTurnConf, tokenOptions: generateTokenOptions(), customToken: customToken}">SAVE</button>
</mat-dialog-actions>
</div>