added more actions and connection list to proctor rooms

This commit is contained in:
anhefti 2020-10-15 16:24:53 +02:00
parent 1d7d15c02c
commit daebd9b5f7
7 changed files with 221 additions and 86 deletions

View file

@ -84,4 +84,29 @@ public class RemoteProctoringRoom {
return builder.toString(); return builder.toString();
} }
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.id == null) ? 0 : this.id.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final RemoteProctoringRoom other = (RemoteProctoringRoom) obj;
if (this.id == null) {
if (other.id != null)
return false;
} else if (!this.id.equals(other.id))
return false;
return true;
}
} }

View file

@ -23,6 +23,7 @@ import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem; import org.eclipse.swt.widgets.TreeItem;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -90,6 +91,7 @@ public class MonitoringRunningExam implements TemplateComposer {
"}"; "}";
// @formatter:on // @formatter:on
private static final String SHOW_CONNECTION_ACTION_APPLIED = "SHOW_CONNECTION_ACTION_APPLIED";
private static final LocTextKey EMPTY_SELECTION_TEXT_KEY = private static final LocTextKey EMPTY_SELECTION_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.emptySelection"); new LocTextKey("sebserver.monitoring.exam.connection.emptySelection");
private static final LocTextKey EMPTY_ACTIVE_SELECTION_TEXT_KEY = private static final LocTextKey EMPTY_ACTIVE_SELECTION_TEXT_KEY =
@ -108,12 +110,14 @@ public class MonitoringRunningExam implements TemplateComposer {
private final GuiServiceInfo guiServiceInfo; private final GuiServiceInfo guiServiceInfo;
private final long pollInterval; private final long pollInterval;
private final String remoteProctoringEndpoint; private final String remoteProctoringEndpoint;
private final ProctorRoomConnectionsPopup proctorRoomConnectionsPopup;
protected MonitoringRunningExam( protected MonitoringRunningExam(
final ServerPushService serverPushService, final ServerPushService serverPushService,
final PageService pageService, final PageService pageService,
final InstructionProcessor instructionProcessor, final InstructionProcessor instructionProcessor,
final GuiServiceInfo guiServiceInfo, final GuiServiceInfo guiServiceInfo,
final ProctorRoomConnectionsPopup proctorRoomConnectionsPopup,
@Value("${sebserver.gui.webservice.poll-interval:1000}") final long pollInterval, @Value("${sebserver.gui.webservice.poll-interval:1000}") final long pollInterval,
@Value("${sebserver.gui.remote.proctoring.entrypoint:/remote-proctoring}") final String remoteProctoringEndpoint) { @Value("${sebserver.gui.remote.proctoring.entrypoint:/remote-proctoring}") final String remoteProctoringEndpoint) {
@ -124,6 +128,7 @@ public class MonitoringRunningExam implements TemplateComposer {
this.guiServiceInfo = guiServiceInfo; this.guiServiceInfo = guiServiceInfo;
this.pollInterval = pollInterval; this.pollInterval = pollInterval;
this.remoteProctoringEndpoint = remoteProctoringEndpoint; this.remoteProctoringEndpoint = remoteProctoringEndpoint;
this.proctorRoomConnectionsPopup = proctorRoomConnectionsPopup;
} }
@Override @Override
@ -315,10 +320,11 @@ public class MonitoringRunningExam implements TemplateComposer {
.getOr(null); .getOr(null);
if (proctoringSettings != null && proctoringSettings.enableProctoring) { if (proctoringSettings != null && proctoringSettings.enableProctoring) {
final Map<String, TreeItem> availableRoomNames = new HashMap<>(); final Map<RemoteProctoringRoom, TreeItem> availableRooms = new HashMap<>();
updateRoomActions( updateRoomActions(
entityKey, entityKey,
availableRoomNames, pageContext,
availableRooms,
actionBuilder, actionBuilder,
proctoringSettings); proctoringSettings);
this.serverPushService.runServerPush( this.serverPushService.runServerPush(
@ -326,7 +332,8 @@ public class MonitoringRunningExam implements TemplateComposer {
5000, 5000,
context -> updateRoomActions( context -> updateRoomActions(
entityKey, entityKey,
availableRoomNames, pageContext,
availableRooms,
actionBuilder, actionBuilder,
proctoringSettings)); proctoringSettings));
} }
@ -334,7 +341,8 @@ public class MonitoringRunningExam implements TemplateComposer {
private void updateRoomActions( private void updateRoomActions(
final EntityKey entityKey, final EntityKey entityKey,
final Map<String, TreeItem> rooms, final PageContext pageContext,
final Map<RemoteProctoringRoom, TreeItem> rooms,
final PageActionBuilder actionBuilder, final PageActionBuilder actionBuilder,
final ProctoringSettings proctoringSettings) { final ProctoringSettings proctoringSettings) {
@ -345,9 +353,9 @@ public class MonitoringRunningExam implements TemplateComposer {
.getOrThrow() .getOrThrow()
.stream() .stream()
.forEach(room -> { .forEach(room -> {
if (rooms.containsKey(room.name)) { if (rooms.containsKey(room)) {
// update action // update action
final TreeItem treeItem = rooms.get(room.name); final TreeItem treeItem = rooms.get(room);
treeItem.setText(i18nSupport.getText(new LocTextKey( treeItem.setText(i18nSupport.getText(new LocTextKey(
ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM.title.name, ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM.title.name,
room.subject, room.subject,
@ -367,10 +375,36 @@ public class MonitoringRunningExam implements TemplateComposer {
.noEventPropagation() .noEventPropagation()
.create(); .create();
this.pageService.publishAction(action, treeItem -> rooms.put(room.name, treeItem)); this.pageService.publishAction(action, treeItem -> rooms.put(room, treeItem));
// check and add show connections in room action adaptation
if (!rooms.isEmpty()) {
final TreeItem treeItem = rooms.values().iterator().next();
final Tree tree = treeItem.getParent();
if (tree.getData(SHOW_CONNECTION_ACTION_APPLIED) == null) {
tree.addListener(SWT.Selection, event -> {
final TreeItem item = (TreeItem) event.item;
treeItem.getParent().deselectAll();
if (event.button == 3) {
rooms.entrySet()
.stream()
.filter(e -> e.getValue().equals(item))
.findFirst()
.ifPresent(e -> {
final PageContext pc = pageContext.copy()
.clearAttributes()
.withEntityKey(new EntityKey(e.getKey().getName(),
EntityType.REMOTE_PROCTORING_ROOM))
.withParentEntityKey(entityKey);
this.proctorRoomConnectionsPopup.show(pc, e.getKey().getSubject());
});
}
});
tree.setData(SHOW_CONNECTION_ACTION_APPLIED, true);
}
}
} }
}); });
} }
private PageAction showExamProctoringRoom( private PageAction showExamProctoringRoom(

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gui.content;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnections;
@Lazy
@Component
@GuiProfile
public class ProctorRoomConnectionsPopup {
private static final LocTextKey TITLE_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.proctoring.room.connections.title");
private final PageService pageService;
protected ProctorRoomConnectionsPopup(final PageService pageService) {
this.pageService = pageService;
}
public void show(final PageContext pageContext, final String roomSubject) {
final ModalInputDialog<Void> dialog = new ModalInputDialog<>(
pageContext.getParent().getShell(),
this.pageService.getWidgetFactory());
dialog.setLargeDialogWidth();
dialog.open(
new LocTextKey(TITLE_TEXT_KEY.name, roomSubject),
pageContext,
this::compose);
}
private void compose(final PageContext pageContext) {
final Composite parent = pageContext.getParent();
final Composite grid = this.pageService.getWidgetFactory().createPopupScrollComposite(parent);
final EntityKey entityKey = pageContext.getEntityKey();
final EntityKey parentEntityKey = pageContext.getParentEntityKey();
this.pageService.getRestService().getBuilder(GetProctorRoomConnections.class)
.withURIVariable(API.PARAM_MODEL_ID, parentEntityKey.modelId)
.withQueryParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, entityKey.modelId)
.call()
.getOrThrow()
.stream()
.forEach(connection -> {
final Label label = new Label(grid, SWT.NONE);
label.setText(connection.userSessionId);
});
}
}

View file

@ -42,19 +42,19 @@ import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
public class JitsiMeetProctoringView implements RemoteProctoringView { public class JitsiMeetProctoringView implements RemoteProctoringView {
private static final LocTextKey CLOSE_WINDOW_TEXT_KEY = private static final LocTextKey CLOSE_WINDOW_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.action.close"); new LocTextKey("sebserver.monitoring.exam.proctoring.action.close");
private static final LocTextKey BROADCAST_AUDIO_ON_TEXT_KEY = private static final LocTextKey BROADCAST_AUDIO_ON_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.action.broadcaston.audio"); new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcaston.audio");
private static final LocTextKey BROADCAST_AUDIO_OFF_TEXT_KEY = private static final LocTextKey BROADCAST_AUDIO_OFF_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.action.broadcastoff.audio"); new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcastoff.audio");
private static final LocTextKey BROADCAST_VIDEO_ON_TEXT_KEY = private static final LocTextKey BROADCAST_VIDEO_ON_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.action.broadcaston.video"); new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcaston.video");
private static final LocTextKey BROADCAST_VIDEO_OFF_TEXT_KEY = private static final LocTextKey BROADCAST_VIDEO_OFF_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.action.broadcastoff.video"); new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcastoff.video");
private static final LocTextKey CHAT_ON_TEXT_KEY = private static final LocTextKey CHAT_ON_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.action.broadcaston.chat"); new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcaston.chat");
private static final LocTextKey CHAT_OFF_TEXT_KEY = private static final LocTextKey CHAT_OFF_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.action.broadcastoff.chat"); new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcastoff.chat");
private final PageService pageService; private final PageService pageService;
private final GuiServiceInfo guiServiceInfo; private final GuiServiceInfo guiServiceInfo;
@ -132,7 +132,8 @@ public class JitsiMeetProctoringView implements RemoteProctoringView {
broadcastVideoAction.addListener(SWT.Selection, event -> toggleBroadcastVideo( broadcastVideoAction.addListener(SWT.Selection, event -> toggleBroadcastVideo(
proctoringWindowData.examId, proctoringWindowData.examId,
proctoringWindowData.connectionData.roomName, proctoringWindowData.connectionData.roomName,
broadcastVideoAction)); broadcastVideoAction,
broadcastAudioAction));
broadcastVideoAction.setData(BroadcastActionState.KEY_NAME, broadcastActionState); broadcastVideoAction.setData(BroadcastActionState.KEY_NAME, broadcastActionState);
final Button chatAction = widgetFactory.buttonLocalized(footer, CHAT_ON_TEXT_KEY); final Button chatAction = widgetFactory.buttonLocalized(footer, CHAT_ON_TEXT_KEY);
@ -172,24 +173,37 @@ public class JitsiMeetProctoringView implements RemoteProctoringView {
state.audio = !state.audio; state.audio = !state.audio;
} }
private void toggleBroadcastVideo(final String examId, final String roomName, final Button broadcastAction) { private void toggleBroadcastVideo(
final String examId,
final String roomName,
final Button videoAction,
final Button audioAction) {
final BroadcastActionState state = final BroadcastActionState state =
(BroadcastActionState) broadcastAction.getData(BroadcastActionState.KEY_NAME); (BroadcastActionState) videoAction.getData(BroadcastActionState.KEY_NAME);
if (state.video) { if (state.video) {
this.pageService.getPolyglotPageService().injectI18n(broadcastAction, BROADCAST_VIDEO_ON_TEXT_KEY); this.pageService.getPolyglotPageService().injectI18n(audioAction, BROADCAST_AUDIO_ON_TEXT_KEY);
this.pageService.getPolyglotPageService().injectI18n(videoAction, BROADCAST_VIDEO_ON_TEXT_KEY);
this.pageService.getRestService().getBuilder(SendProctoringBroadcastOffInstruction.class) this.pageService.getRestService().getBuilder(SendProctoringBroadcastOffInstruction.class)
.withURIVariable(API.PARAM_MODEL_ID, examId) .withURIVariable(API.PARAM_MODEL_ID, examId)
.withFormParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, roomName) .withFormParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, roomName)
.withFormParam(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_AUDIO,
Constants.TRUE_STRING)
.withFormParam( .withFormParam(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_VIDEO, ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_VIDEO,
Constants.TRUE_STRING) Constants.TRUE_STRING)
.call() .call()
.getOrThrow(); .getOrThrow();
} else { } else {
this.pageService.getPolyglotPageService().injectI18n(broadcastAction, BROADCAST_VIDEO_OFF_TEXT_KEY); this.pageService.getPolyglotPageService().injectI18n(audioAction, BROADCAST_AUDIO_OFF_TEXT_KEY);
this.pageService.getPolyglotPageService().injectI18n(videoAction, BROADCAST_VIDEO_OFF_TEXT_KEY);
this.pageService.getRestService().getBuilder(SendProctoringBroadcastOnInstruction.class) this.pageService.getRestService().getBuilder(SendProctoringBroadcastOnInstruction.class)
.withURIVariable(API.PARAM_MODEL_ID, examId) .withURIVariable(API.PARAM_MODEL_ID, examId)
.withFormParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, roomName) .withFormParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, roomName)
.withFormParam(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_AUDIO,
Constants.TRUE_STRING)
.withFormParam( .withFormParam(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_VIDEO, ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_RECONFIGURE_SETTINGS.JITSI_RECEIVE_VIDEO,
Constants.TRUE_STRING) Constants.TRUE_STRING)
@ -197,6 +211,7 @@ public class JitsiMeetProctoringView implements RemoteProctoringView {
.getOrThrow(); .getOrThrow();
} }
state.video = !state.video; state.video = !state.video;
state.audio = state.video;
} }
private void toggleChat(final String examId, final String roomName, final Button broadcastAction) { private void toggleChat(final String examId, final String roomName, final Button broadcastAction) {

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session;
import java.util.Collection;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class GetProctorRoomConnections extends RestCall<Collection<ClientConnection>> {
public GetProctorRoomConnections() {
super(new TypeKey<>(
CallType.GET_LIST,
EntityType.CLIENT_CONNECTION,
new TypeReference<Collection<ClientConnection>>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_MONITORING_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTORING_PATH_SEGMENT
+ API.PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT);
}
}

View file

@ -360,7 +360,7 @@ public class ExamMonitoringController {
@PathVariable(name = API.PARAM_MODEL_ID) final Long examId, @PathVariable(name = API.PARAM_MODEL_ID) final Long examId,
@RequestParam( @RequestParam(
name = Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, name = Domain.REMOTE_PROCTORING_ROOM.ATTR_ID,
required = true) final Long roomId) { required = true) final String roomName) {
this.authorization.check( this.authorization.check(
PrivilegeType.READ, PrivilegeType.READ,
@ -370,7 +370,7 @@ public class ExamMonitoringController {
this.authorization.checkRead( this.authorization.checkRead(
this.examSessionService.getExamDAO().byPK(examId).getOrThrow()); this.examSessionService.getExamDAO().byPK(examId).getOrThrow());
return this.examProcotringRoomService.getRoomConnections(roomId) return this.examProcotringRoomService.getRoomConnections(examId, roomName)
.getOrThrow(); .getOrThrow();
} }
@ -639,62 +639,6 @@ public class ExamMonitoringController {
.getOrThrow(); .getOrThrow();
} }
// @RequestMapping(
// path = API.MODEL_ID_VAR_PATH_SEGMENT
// + API.PROCTORING_PATH_SEGMENT
// + API.PROCTORING_LEAVE_ROOM_PATH_SEGMENT,
// method = RequestMethod.POST,
// produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
// public List<SEBProctoringConnectionData> leaveProctoringRoom(
// @RequestParam(
// name = API.PARAM_INSTITUTION_ID,
// required = true,
// defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
// @PathVariable(name = API.PARAM_MODEL_ID) final Long examId,
// @RequestParam(
// name = SEBProctoringConnectionData.ATTR_ROOM_NAME,
// required = true) final String roomName,
// @RequestParam(
// name = API.EXAM_API_SEB_CONNECTION_TOKEN,
// required = true) final String connectionTokens) {
//
// this.authorization.check(
// PrivilegeType.READ,
// EntityType.EXAM,
// institutionId);
//
// final ProctoringSettings settings = this.examSessionService
// .getRunningExam(examId)
// .flatMap(this.authorization::checkRead)
// .flatMap(this.examAdminService::getExamProctoring)
// .getOrThrow();
//
// final ExamProctoringService examProctoringService = this.examAdminService
// .getExamProctoringService(settings.serverType)
// .getOrThrow();
//
// if (StringUtils.isNotBlank(connectionTokens)) {
// return (connectionTokens.contains(Constants.LIST_SEPARATOR)
// ? Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR))
// : Arrays.asList(connectionTokens))
// .stream()
// .map(connectionToken -> {
// final SEBProctoringConnectionData data = examProctoringService
// .createClientPublicRoomConnection(settings, connectionToken, roomName, roomName)
// .getOrThrow();
//
// sendLeaveInstruction(examId, connectionTokens, data)
// .onError(error -> log.error(
// "Failed to send proctoring leave instruction for common room to client: {} ",
// connectionToken, error));
// return data;
//
// }).collect(Collectors.toList());
// }
//
// return Collections.emptyList();
// }
//**** Proctoring //**** Proctoring
//*********************************************************************************************** //***********************************************************************************************

View file

@ -1437,17 +1437,19 @@ sebserver.monitoring.exam.list.title=Running Exams
sebserver.monitoring.exam.list.actions= sebserver.monitoring.exam.list.actions=
sebserver.monitoring.exam.action.detail.view=Back To Monitoring sebserver.monitoring.exam.action.detail.view=Back To Monitoring
sebserver.monitoring.exam.action.list.view=Monitoring sebserver.monitoring.exam.action.list.view=Monitoring
sebserver.monitoring.exam.action.close=Close Window sebserver.monitoring.exam.action.viewroom=View {0} ( {1} / {2} )
sebserver.monitoring.exam.action.broadcaston.audio=Start Audio Broadcast
sebserver.monitoring.exam.action.broadcastoff.audio=End Audio Broadcast
sebserver.monitoring.exam.action.broadcaston.video=Start Video Broadcast
sebserver.monitoring.exam.action.broadcastoff.video=End Video Broadcast
sebserver.monitoring.exam.action.broadcaston.chat=Enable Chat
sebserver.monitoring.exam.action.broadcastoff.chat=Disable Chat
sebserver.monitoring.exam.action.viewroom=View {0} | {1} / {2}
sebserver.exam.monitoring.action.category.filter=Filter sebserver.exam.monitoring.action.category.filter=Filter
sebserver.exam.overall.action.category.proctoring=Proctoring sebserver.exam.overall.action.category.proctoring=Proctoring
sebserver.monitoring.exam.proctoring.action.close=Close Window
sebserver.monitoring.exam.proctoring.action.broadcaston.audio=Start Audio Broadcast
sebserver.monitoring.exam.proctoring.action.broadcastoff.audio=End Audio Broadcast
sebserver.monitoring.exam.proctoring.action.broadcaston.video=Start Video Broadcast
sebserver.monitoring.exam.proctoring.action.broadcastoff.video=End Video Broadcast
sebserver.monitoring.exam.proctoring.action.broadcaston.chat=Enable Chat
sebserver.monitoring.exam.proctoring.action.broadcastoff.chat=Disable Chat
sebserver.monitoring.exam.proctoring.room.connections.title=SEB Connections in {0}
sebserver.monitoring.exam.info.pleaseSelect=At first please select an Exam from the list sebserver.monitoring.exam.info.pleaseSelect=At first please select an Exam from the list
sebserver.monitoring.exam.list.empty=There are currently no running exams sebserver.monitoring.exam.list.empty=There are currently no running exams