mirror of https://github.com/OpenVidu/openvidu.git
openvidu-server: webhook
parent
d72063b97d
commit
9c4941de9a
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -28,6 +28,4 @@ public interface CDRLogger {
|
|||
|
||||
public void log(SessionSummary sessionSummary);
|
||||
|
||||
public boolean canBeDisabled();
|
||||
|
||||
}
|
||||
|
|
|
@ -40,9 +40,4 @@ public class CDRLoggerFile implements CDRLogger {
|
|||
public void log(SessionSummary sessionSummary) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canBeDisabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)");
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue