SEBSP-15, SEBSP-70: Implemented basic screen proctoring functionality including image format & quantization settings.
This commit is contained in:
parent
70ba9ad7b6
commit
acb6b8cf09
47 changed files with 1787 additions and 102 deletions
|
@ -6,6 +6,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using SafeExamBrowser.Settings;
|
||||
using SafeExamBrowser.Settings.Proctoring;
|
||||
|
||||
|
@ -74,6 +75,45 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping
|
|||
case Keys.Proctoring.JitsiMeet.VideoMuted:
|
||||
MapJitsiMeetVideoMuted(settings, value);
|
||||
break;
|
||||
case Keys.Proctoring.ScreenProctoring.CaptureApplicationName:
|
||||
MapCaptureApplicationName(settings, value);
|
||||
break;
|
||||
case Keys.Proctoring.ScreenProctoring.CaptureBrowserUrl:
|
||||
MapCaptureBrowserUrl(settings, value);
|
||||
break;
|
||||
case Keys.Proctoring.ScreenProctoring.CaptureWindowTitle:
|
||||
MapCaptureWindowTitle(settings, value);
|
||||
break;
|
||||
case Keys.Proctoring.ScreenProctoring.ClientId:
|
||||
MapClientId(settings, value);
|
||||
break;
|
||||
case Keys.Proctoring.ScreenProctoring.ClientSecret:
|
||||
MapClientSecret(settings, value);
|
||||
break;
|
||||
case Keys.Proctoring.ScreenProctoring.GroupId:
|
||||
MapGroupId(settings, value);
|
||||
break;
|
||||
case Keys.Proctoring.ScreenProctoring.ImageDownscaling:
|
||||
MapImageDownscaling(settings, value);
|
||||
break;
|
||||
case Keys.Proctoring.ScreenProctoring.ImageFormat:
|
||||
MapImageFormat(settings, value);
|
||||
break;
|
||||
case Keys.Proctoring.ScreenProctoring.ImageQuantization:
|
||||
MapImageQuantization(settings, value);
|
||||
break;
|
||||
case Keys.Proctoring.ScreenProctoring.MaxInterval:
|
||||
MapMaxInterval(settings, value);
|
||||
break;
|
||||
case Keys.Proctoring.ScreenProctoring.MinInterval:
|
||||
MapMinInterval(settings, value);
|
||||
break;
|
||||
case Keys.Proctoring.ScreenProctoring.Enabled:
|
||||
MapScreenProctoringEnabled(settings, value);
|
||||
break;
|
||||
case Keys.Proctoring.ScreenProctoring.ServiceUrl:
|
||||
MapServiceUrl(settings, value);
|
||||
break;
|
||||
case Keys.Proctoring.ShowRaiseHand:
|
||||
MapShowRaiseHand(settings, value);
|
||||
break;
|
||||
|
@ -280,6 +320,134 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping
|
|||
}
|
||||
}
|
||||
|
||||
private void MapCaptureApplicationName(AppSettings settings, object value)
|
||||
{
|
||||
if (value is bool capture)
|
||||
{
|
||||
settings.Proctoring.ScreenProctoring.CaptureApplicationName = capture;
|
||||
}
|
||||
}
|
||||
|
||||
private void MapCaptureBrowserUrl(AppSettings settings, object value)
|
||||
{
|
||||
if (value is bool capture)
|
||||
{
|
||||
settings.Proctoring.ScreenProctoring.CaptureBrowserUrl = capture;
|
||||
}
|
||||
}
|
||||
|
||||
private void MapCaptureWindowTitle(AppSettings settings, object value)
|
||||
{
|
||||
if (value is bool capture)
|
||||
{
|
||||
settings.Proctoring.ScreenProctoring.CaptureWindowTitle = capture;
|
||||
}
|
||||
}
|
||||
|
||||
private void MapClientId(AppSettings settings, object value)
|
||||
{
|
||||
if (value is string clientId)
|
||||
{
|
||||
settings.Proctoring.ScreenProctoring.ClientId = clientId;
|
||||
}
|
||||
}
|
||||
|
||||
private void MapClientSecret(AppSettings settings, object value)
|
||||
{
|
||||
if (value is string secret)
|
||||
{
|
||||
settings.Proctoring.ScreenProctoring.ClientSecret = secret;
|
||||
}
|
||||
}
|
||||
|
||||
private void MapGroupId(AppSettings settings, object value)
|
||||
{
|
||||
if (value is string groupId)
|
||||
{
|
||||
settings.Proctoring.ScreenProctoring.GroupId = groupId;
|
||||
}
|
||||
}
|
||||
|
||||
private void MapImageDownscaling(AppSettings settings, object value)
|
||||
{
|
||||
if (value is double downscaling)
|
||||
{
|
||||
settings.Proctoring.ScreenProctoring.ImageDownscaling = downscaling;
|
||||
}
|
||||
}
|
||||
|
||||
private void MapImageFormat(AppSettings settings, object value)
|
||||
{
|
||||
if (value is string s && Enum.TryParse<ImageFormat>(s, true, out var format))
|
||||
{
|
||||
settings.Proctoring.ScreenProctoring.ImageFormat = format;
|
||||
}
|
||||
}
|
||||
|
||||
private void MapImageQuantization(AppSettings settings, object value)
|
||||
{
|
||||
if (value is int quantization)
|
||||
{
|
||||
switch (quantization)
|
||||
{
|
||||
case 0:
|
||||
settings.Proctoring.ScreenProctoring.ImageQuantization = ImageQuantization.BlackAndWhite1bpp;
|
||||
break;
|
||||
case 1:
|
||||
settings.Proctoring.ScreenProctoring.ImageQuantization = ImageQuantization.Grayscale2bpp;
|
||||
break;
|
||||
case 2:
|
||||
settings.Proctoring.ScreenProctoring.ImageQuantization = ImageQuantization.Grayscale4bpp;
|
||||
break;
|
||||
case 3:
|
||||
settings.Proctoring.ScreenProctoring.ImageQuantization = ImageQuantization.Grayscale8bpp;
|
||||
break;
|
||||
case 4:
|
||||
settings.Proctoring.ScreenProctoring.ImageQuantization = ImageQuantization.Color8bpp;
|
||||
break;
|
||||
case 5:
|
||||
settings.Proctoring.ScreenProctoring.ImageQuantization = ImageQuantization.Color16bpp;
|
||||
break;
|
||||
case 6:
|
||||
settings.Proctoring.ScreenProctoring.ImageQuantization = ImageQuantization.Color24bpp;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void MapMaxInterval(AppSettings settings, object value)
|
||||
{
|
||||
if (value is int interval)
|
||||
{
|
||||
settings.Proctoring.ScreenProctoring.MaxInterval = interval;
|
||||
}
|
||||
}
|
||||
|
||||
private void MapMinInterval(AppSettings settings, object value)
|
||||
{
|
||||
if (value is int interval)
|
||||
{
|
||||
settings.Proctoring.ScreenProctoring.MinInterval = interval;
|
||||
}
|
||||
}
|
||||
|
||||
private void MapScreenProctoringEnabled(AppSettings settings, object value)
|
||||
{
|
||||
if (value is bool enabled)
|
||||
{
|
||||
settings.Proctoring.ScreenProctoring.Enabled = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
private void MapServiceUrl(AppSettings settings, object value)
|
||||
{
|
||||
if (value is string url)
|
||||
{
|
||||
settings.Proctoring.ScreenProctoring.ServiceUrl = url;
|
||||
}
|
||||
}
|
||||
|
||||
private void MapShowRaiseHand(AppSettings settings, object value)
|
||||
{
|
||||
if (value is bool show)
|
||||
|
|
|
@ -76,7 +76,7 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
|
|||
|
||||
private void InitializeProctoringSettings(AppSettings settings)
|
||||
{
|
||||
settings.Proctoring.Enabled = settings.Proctoring.JitsiMeet.Enabled;
|
||||
settings.Proctoring.Enabled = settings.Proctoring.JitsiMeet.Enabled || settings.Proctoring.ScreenProctoring.Enabled;
|
||||
|
||||
if (settings.Proctoring.JitsiMeet.Enabled && !settings.Proctoring.JitsiMeet.ReceiveVideo)
|
||||
{
|
||||
|
|
|
@ -257,6 +257,15 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
|
|||
settings.Proctoring.JitsiMeet.SendVideo = true;
|
||||
settings.Proctoring.JitsiMeet.ShowMeetingName = false;
|
||||
settings.Proctoring.JitsiMeet.VideoMuted = false;
|
||||
settings.Proctoring.ScreenProctoring.CaptureApplicationName = true;
|
||||
settings.Proctoring.ScreenProctoring.CaptureBrowserUrl = true;
|
||||
settings.Proctoring.ScreenProctoring.CaptureWindowTitle = true;
|
||||
settings.Proctoring.ScreenProctoring.Enabled = false;
|
||||
settings.Proctoring.ScreenProctoring.ImageDownscaling = 1.0;
|
||||
settings.Proctoring.ScreenProctoring.ImageFormat = ImageFormat.Png;
|
||||
settings.Proctoring.ScreenProctoring.ImageQuantization = ImageQuantization.Grayscale4bpp;
|
||||
settings.Proctoring.ScreenProctoring.MaxInterval = 5000;
|
||||
settings.Proctoring.ScreenProctoring.MinInterval = 1000;
|
||||
settings.Proctoring.ShowRaiseHandNotification = true;
|
||||
settings.Proctoring.ShowTaskbarNotification = true;
|
||||
settings.Proctoring.WindowVisibility = WindowVisibility.Hidden;
|
||||
|
|
|
@ -256,6 +256,23 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
|
|||
internal const string VideoMuted = "jitsiMeetVideoMuted";
|
||||
}
|
||||
|
||||
internal static class ScreenProctoring
|
||||
{
|
||||
internal const string CaptureApplicationName = "screenProctoringMetadataActiveAppEnabled";
|
||||
internal const string CaptureBrowserUrl = "screenProctoringMetadataURLEnabled";
|
||||
internal const string CaptureWindowTitle = "screenProctoringMetadataWindowTitleEnabled";
|
||||
internal const string ClientId = "screenProctoringClientId";
|
||||
internal const string ClientSecret = "screenProctoringClientSecret";
|
||||
internal const string Enabled = "enableScreenProctoring";
|
||||
internal const string GroupId = "screenProctoringGroupId";
|
||||
internal const string ImageDownscaling = "screenProctoringImageDownscale";
|
||||
internal const string ImageFormat = "screenProctoringImageFormat";
|
||||
internal const string ImageQuantization = "screenProctoringImageQuantization";
|
||||
internal const string MaxInterval = "screenProctoringScreenshotMaxInterval";
|
||||
internal const string MinInterval = "screenProctoringScreenshotMinInterval";
|
||||
internal const string ServiceUrl = "screenProctoringServiceURL";
|
||||
}
|
||||
|
||||
internal static class Zoom
|
||||
{
|
||||
internal const string AllowChat = "zoomFeatureFlagChat";
|
||||
|
|
|
@ -17,7 +17,7 @@ using SafeExamBrowser.Core.Contracts.Notifications.Events;
|
|||
using SafeExamBrowser.Core.Contracts.Resources.Icons;
|
||||
using SafeExamBrowser.I18n.Contracts;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
using SafeExamBrowser.Server.Contracts.Events;
|
||||
using SafeExamBrowser.Server.Contracts.Events.Proctoring;
|
||||
using SafeExamBrowser.Settings.Proctoring;
|
||||
using SafeExamBrowser.SystemComponents.Contracts;
|
||||
using SafeExamBrowser.UserInterface.Contracts;
|
||||
|
@ -41,9 +41,6 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet
|
|||
|
||||
internal override string Name => nameof(JitsiMeet);
|
||||
|
||||
public override string Tooltip { get; protected set; }
|
||||
public override IconResource IconResource { get; protected set; }
|
||||
|
||||
public override event NotificationChangedEventHandler NotificationChanged;
|
||||
|
||||
internal JitsiMeetImplementation(
|
||||
|
@ -60,21 +57,6 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet
|
|||
this.settings = settings;
|
||||
this.text = text;
|
||||
this.uiFactory = uiFactory;
|
||||
|
||||
IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/ProctoringNotification_Inactive.xaml") };
|
||||
Tooltip = text.Get(TextKey.Notification_ProctoringInactiveTooltip);
|
||||
}
|
||||
|
||||
public override void Activate()
|
||||
{
|
||||
if (settings.WindowVisibility == WindowVisibility.Visible)
|
||||
{
|
||||
window?.BringToForeground();
|
||||
}
|
||||
else if (settings.WindowVisibility == WindowVisibility.AllowToHide || settings.WindowVisibility == WindowVisibility.AllowToShow)
|
||||
{
|
||||
window?.Toggle();
|
||||
}
|
||||
}
|
||||
|
||||
internal override void Initialize()
|
||||
|
@ -87,11 +69,15 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet
|
|||
start &= !string.IsNullOrWhiteSpace(settings.JitsiMeet.RoomName);
|
||||
start &= !string.IsNullOrWhiteSpace(settings.JitsiMeet.ServerUrl);
|
||||
|
||||
logger.Info("Initialized proctoring.");
|
||||
|
||||
if (start)
|
||||
{
|
||||
StartProctoring();
|
||||
logger.Info($"Initialized proctoring: All settings are valid, starting automatically...");
|
||||
Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowNotificationInactive();
|
||||
logger.Info($"Initialized proctoring: Not all settings are valid or a server session is active, not starting automatically.");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,27 +98,37 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet
|
|||
settings.WindowVisibility = initialVisibility;
|
||||
}
|
||||
|
||||
StopProctoring();
|
||||
StartProctoring();
|
||||
Stop();
|
||||
Start();
|
||||
|
||||
logger.Info($"Successfully updated configuration: {nameof(allowChat)}={allowChat}, {nameof(receiveAudio)}={receiveAudio}, {nameof(receiveVideo)}={receiveVideo}.");
|
||||
}
|
||||
|
||||
internal override void ProctoringInstructionReceived(ProctoringInstructionEventArgs args)
|
||||
internal override void ProctoringInstructionReceived(InstructionEventArgs args)
|
||||
{
|
||||
logger.Info("Proctoring instruction received.");
|
||||
if (args is JitsiMeetInstruction instruction)
|
||||
{
|
||||
logger.Info($"Proctoring instruction received: {instruction.Method}");
|
||||
|
||||
settings.JitsiMeet.RoomName = args.JitsiMeetRoomName;
|
||||
settings.JitsiMeet.ServerUrl = args.JitsiMeetServerUrl;
|
||||
settings.JitsiMeet.Token = args.JitsiMeetToken;
|
||||
if (instruction.Method == InstructionMethod.Join)
|
||||
{
|
||||
settings.JitsiMeet.RoomName = instruction.RoomName;
|
||||
settings.JitsiMeet.ServerUrl = instruction.ServerUrl;
|
||||
settings.JitsiMeet.Token = instruction.Token;
|
||||
|
||||
StopProctoring();
|
||||
StartProctoring();
|
||||
Stop();
|
||||
Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
logger.Info("Successfully processed instruction.");
|
||||
logger.Info("Successfully processed instruction.");
|
||||
}
|
||||
}
|
||||
|
||||
internal override void StartProctoring()
|
||||
internal override void Start()
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
|
@ -173,7 +169,7 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet
|
|||
});
|
||||
}
|
||||
|
||||
internal override void StopProctoring()
|
||||
internal override void Stop()
|
||||
{
|
||||
if (control != default && window != default)
|
||||
{
|
||||
|
@ -197,9 +193,28 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet
|
|||
|
||||
internal override void Terminate()
|
||||
{
|
||||
Stop();
|
||||
TerminateNotification();
|
||||
logger.Info("Terminated proctoring.");
|
||||
}
|
||||
|
||||
protected override void ActivateNotification()
|
||||
{
|
||||
if (settings.WindowVisibility == WindowVisibility.Visible)
|
||||
{
|
||||
window?.BringToForeground();
|
||||
}
|
||||
else if (settings.WindowVisibility == WindowVisibility.AllowToHide || settings.WindowVisibility == WindowVisibility.AllowToShow)
|
||||
{
|
||||
window?.Toggle();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void TerminateNotification()
|
||||
{
|
||||
// Nothing to do here for now.
|
||||
}
|
||||
|
||||
private string LoadContent(ProctoringSettings settings)
|
||||
{
|
||||
var assembly = Assembly.GetAssembly(typeof(ProctoringController));
|
||||
|
|
|
@ -15,7 +15,7 @@ using SafeExamBrowser.Logging.Contracts;
|
|||
using SafeExamBrowser.Proctoring.Contracts;
|
||||
using SafeExamBrowser.Proctoring.Contracts.Events;
|
||||
using SafeExamBrowser.Server.Contracts;
|
||||
using SafeExamBrowser.Server.Contracts.Events;
|
||||
using SafeExamBrowser.Server.Contracts.Events.Proctoring;
|
||||
using SafeExamBrowser.Settings.Proctoring;
|
||||
using SafeExamBrowser.SystemComponents.Contracts;
|
||||
using SafeExamBrowser.UserInterface.Contracts;
|
||||
|
@ -144,7 +144,7 @@ namespace SafeExamBrowser.Proctoring
|
|||
}
|
||||
}
|
||||
|
||||
private void Server_ProctoringInstructionReceived(ProctoringInstructionEventArgs args)
|
||||
private void Server_ProctoringInstructionReceived(InstructionEventArgs args)
|
||||
{
|
||||
foreach (var implementation in implementations)
|
||||
{
|
||||
|
|
|
@ -11,6 +11,8 @@ using SafeExamBrowser.Configuration.Contracts;
|
|||
using SafeExamBrowser.I18n.Contracts;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
using SafeExamBrowser.Proctoring.JitsiMeet;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Service;
|
||||
using SafeExamBrowser.Settings.Proctoring;
|
||||
using SafeExamBrowser.SystemComponents.Contracts;
|
||||
using SafeExamBrowser.UserInterface.Contracts;
|
||||
|
@ -40,7 +42,17 @@ namespace SafeExamBrowser.Proctoring
|
|||
|
||||
if (settings.JitsiMeet.Enabled)
|
||||
{
|
||||
implementations.Add(new JitsiMeetImplementation(appConfig, fileSystem, logger.CloneFor(nameof(JitsiMeet)), settings, text, uiFactory));
|
||||
var logger = this.logger.CloneFor(nameof(JitsiMeet));
|
||||
|
||||
implementations.Add(new JitsiMeetImplementation(appConfig, fileSystem, logger, settings, text, uiFactory));
|
||||
}
|
||||
|
||||
if (settings.ScreenProctoring.Enabled)
|
||||
{
|
||||
var logger = this.logger.CloneFor(nameof(ScreenProctoring));
|
||||
var service = new ServiceProxy(logger.CloneFor(nameof(ServiceProxy)));
|
||||
|
||||
implementations.Add(new ScreenProctoringImplementation(logger, service, settings, text));
|
||||
}
|
||||
|
||||
return implementations;
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
using SafeExamBrowser.Core.Contracts.Notifications;
|
||||
using SafeExamBrowser.Core.Contracts.Notifications.Events;
|
||||
using SafeExamBrowser.Core.Contracts.Resources.Icons;
|
||||
using SafeExamBrowser.Server.Contracts.Events;
|
||||
using SafeExamBrowser.Server.Contracts.Events.Proctoring;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring
|
||||
{
|
||||
|
@ -17,19 +17,29 @@ namespace SafeExamBrowser.Proctoring
|
|||
{
|
||||
internal abstract string Name { get; }
|
||||
|
||||
public abstract string Tooltip { get; protected set; }
|
||||
public abstract IconResource IconResource { get; protected set; }
|
||||
public string Tooltip { get; protected set; }
|
||||
public IconResource IconResource { get; protected set; }
|
||||
|
||||
public abstract event NotificationChangedEventHandler NotificationChanged;
|
||||
|
||||
public abstract void Activate();
|
||||
void INotification.Terminate() { }
|
||||
void INotification.Activate()
|
||||
{
|
||||
ActivateNotification();
|
||||
}
|
||||
|
||||
void INotification.Terminate()
|
||||
{
|
||||
TerminateNotification();
|
||||
}
|
||||
|
||||
internal abstract void Initialize();
|
||||
internal abstract void ProctoringConfigurationReceived(bool allowChat, bool receiveAudio, bool receiveVideo);
|
||||
internal abstract void ProctoringInstructionReceived(ProctoringInstructionEventArgs args);
|
||||
internal abstract void StartProctoring();
|
||||
internal abstract void StopProctoring();
|
||||
internal abstract void ProctoringInstructionReceived(InstructionEventArgs args);
|
||||
internal abstract void Start();
|
||||
internal abstract void Stop();
|
||||
internal abstract void Terminate();
|
||||
|
||||
protected abstract void ActivateNotification();
|
||||
protected abstract void TerminateNotification();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,15 @@
|
|||
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="KGySoft.CoreLibraries, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b45eba277439ddfe, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\KGySoft.CoreLibraries.8.0.0\lib\net472\KGySoft.CoreLibraries.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="KGySoft.Drawing, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b45eba277439ddfe, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\KGySoft.Drawing.8.0.0\lib\net46\KGySoft.Drawing.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="KGySoft.Drawing.Core, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b45eba277439ddfe, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\KGySoft.Drawing.Core.8.0.0\lib\net46\KGySoft.Drawing.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Web.WebView2.Core, Version=1.0.2088.41, Culture=neutral, PublicKeyToken=2a8ab48044d2601e, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Microsoft.Web.WebView2.1.0.2088.41\lib\net45\Microsoft.Web.WebView2.Core.dll</HintPath>
|
||||
</Reference>
|
||||
|
@ -69,6 +78,9 @@
|
|||
<Reference Include="PresentationFramework" />
|
||||
<Reference Include="System" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xaml" />
|
||||
<Reference Include="WindowsBase" />
|
||||
</ItemGroup>
|
||||
|
@ -79,6 +91,22 @@
|
|||
<Compile Include="ProctoringFactory.cs" />
|
||||
<Compile Include="ProctoringImplementation.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="ScreenProctoring\Imaging\Extensions.cs" />
|
||||
<Compile Include="ScreenProctoring\Imaging\ProcessingOrder.cs" />
|
||||
<Compile Include="ScreenProctoring\Imaging\ScreenShot.cs" />
|
||||
<Compile Include="ScreenProctoring\ScreenProctoringImplementation.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\Api.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\Parser.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\Requests\ContentType.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\Requests\CreateSessionRequest.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\Requests\Header.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\Requests\Extensions.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\Requests\TerminateSessionRequest.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\Requests\OAuth2TokenRequest.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\Requests\Request.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\Requests\ScreenShotRequest.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\ServiceProxy.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\ServiceResponse.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SafeExamBrowser.Configuration.Contracts\SafeExamBrowser.Configuration.Contracts.csproj">
|
||||
|
@ -124,6 +152,7 @@
|
|||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="..\packages\Microsoft.Web.WebView2.1.0.2088.41\build\Microsoft.Web.WebView2.targets" Condition="Exists('..\packages\Microsoft.Web.WebView2.1.0.2088.41\build\Microsoft.Web.WebView2.targets')" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Windows.Forms;
|
||||
using KGySoft.Drawing.Imaging;
|
||||
using SafeExamBrowser.Settings.Proctoring;
|
||||
using ImageFormat = SafeExamBrowser.Settings.Proctoring.ImageFormat;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Imaging
|
||||
{
|
||||
internal static class Extensions
|
||||
{
|
||||
internal static void DrawCursorPosition(this Graphics graphics)
|
||||
{
|
||||
graphics.DrawArc(new Pen(Color.Red, 3), Cursor.Position.X - 25, Cursor.Position.Y - 25, 50, 50, 0, 360);
|
||||
graphics.DrawArc(new Pen(Color.Yellow, 3), Cursor.Position.X - 22, Cursor.Position.Y - 22, 44, 44, 0, 360);
|
||||
graphics.FillEllipse(Brushes.Red, Cursor.Position.X - 4, Cursor.Position.Y - 4, 8, 8);
|
||||
graphics.FillEllipse(Brushes.Yellow, Cursor.Position.X - 2, Cursor.Position.Y - 2, 4, 4);
|
||||
}
|
||||
|
||||
internal static PixelFormat ToPixelFormat(this ImageQuantization quantization)
|
||||
{
|
||||
switch (quantization)
|
||||
{
|
||||
case ImageQuantization.BlackAndWhite1bpp:
|
||||
return PixelFormat.Format1bppIndexed;
|
||||
case ImageQuantization.Color8bpp:
|
||||
return PixelFormat.Format8bppIndexed;
|
||||
case ImageQuantization.Color16bpp:
|
||||
return PixelFormat.Format16bppArgb1555;
|
||||
case ImageQuantization.Color24bpp:
|
||||
return PixelFormat.Format24bppRgb;
|
||||
case ImageQuantization.Grayscale2bpp:
|
||||
return PixelFormat.Format4bppIndexed;
|
||||
case ImageQuantization.Grayscale4bpp:
|
||||
return PixelFormat.Format4bppIndexed;
|
||||
case ImageQuantization.Grayscale8bpp:
|
||||
return PixelFormat.Format8bppIndexed;
|
||||
default:
|
||||
throw new NotImplementedException($"Image quantization '{quantization}' is not yet implemented!");
|
||||
}
|
||||
}
|
||||
|
||||
internal static IQuantizer ToQuantizer(this ImageQuantization quantization)
|
||||
{
|
||||
switch (quantization)
|
||||
{
|
||||
case ImageQuantization.BlackAndWhite1bpp:
|
||||
return PredefinedColorsQuantizer.BlackAndWhite();
|
||||
case ImageQuantization.Color8bpp:
|
||||
return PredefinedColorsQuantizer.SystemDefault8BppPalette();
|
||||
case ImageQuantization.Color16bpp:
|
||||
return PredefinedColorsQuantizer.Rgb555();
|
||||
case ImageQuantization.Color24bpp:
|
||||
return PredefinedColorsQuantizer.Rgb888();
|
||||
case ImageQuantization.Grayscale2bpp:
|
||||
return PredefinedColorsQuantizer.Grayscale4();
|
||||
case ImageQuantization.Grayscale4bpp:
|
||||
return PredefinedColorsQuantizer.Grayscale16();
|
||||
case ImageQuantization.Grayscale8bpp:
|
||||
return PredefinedColorsQuantizer.Grayscale();
|
||||
default:
|
||||
throw new NotImplementedException($"Image quantization '{quantization}' is not yet implemented!");
|
||||
}
|
||||
}
|
||||
|
||||
internal static System.Drawing.Imaging.ImageFormat ToSystemFormat(this ImageFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case ImageFormat.Bmp:
|
||||
return System.Drawing.Imaging.ImageFormat.Bmp;
|
||||
case ImageFormat.Gif:
|
||||
return System.Drawing.Imaging.ImageFormat.Gif;
|
||||
case ImageFormat.Jpg:
|
||||
return System.Drawing.Imaging.ImageFormat.Jpeg;
|
||||
case ImageFormat.Png:
|
||||
return System.Drawing.Imaging.ImageFormat.Png;
|
||||
default:
|
||||
throw new NotImplementedException($"Image format '{format}' is not yet implemented!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Imaging
|
||||
{
|
||||
internal enum ProcessingOrder
|
||||
{
|
||||
DownscalingQuantizing,
|
||||
QuantizingDownscaling,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using KGySoft.Drawing;
|
||||
using KGySoft.Drawing.Imaging;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
using SafeExamBrowser.Settings.Proctoring;
|
||||
using ImageFormat = SafeExamBrowser.Settings.Proctoring.ImageFormat;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Imaging
|
||||
{
|
||||
internal class ScreenShot : IDisposable
|
||||
{
|
||||
private readonly ILogger logger;
|
||||
private readonly ScreenProctoringSettings settings;
|
||||
|
||||
internal Bitmap Bitmap { get; private set; }
|
||||
internal byte[] Data { get; private set; }
|
||||
internal ImageFormat Format { get; private set; }
|
||||
internal int Height { get; private set; }
|
||||
internal int Width { get; private set; }
|
||||
|
||||
public ScreenShot(ILogger logger, ScreenProctoringSettings settings)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Bitmap?.Dispose();
|
||||
Bitmap = default;
|
||||
Data = default;
|
||||
}
|
||||
|
||||
public string ToReducedString()
|
||||
{
|
||||
return $"{Width}x{Height}, {Data.Length / 1000:N0} kB, {Format.ToString().ToUpper()}";
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"resolution: {Width}x{Height}, size: {Data.Length / 1000:N0} kB, format: {Format.ToString().ToUpper()}";
|
||||
}
|
||||
|
||||
internal void Compress()
|
||||
{
|
||||
var order = ProcessingOrder.QuantizingDownscaling;
|
||||
var original = ToReducedString();
|
||||
var parameters = $"{order}, {settings.ImageQuantization}, 1:{settings.ImageDownscaling}";
|
||||
|
||||
switch (order)
|
||||
{
|
||||
case ProcessingOrder.DownscalingQuantizing:
|
||||
Downscale();
|
||||
Quantize();
|
||||
Serialize();
|
||||
break;
|
||||
case ProcessingOrder.QuantizingDownscaling:
|
||||
Quantize();
|
||||
Downscale();
|
||||
Serialize();
|
||||
break;
|
||||
}
|
||||
|
||||
logger.Debug($"Compressed from '{original}' to '{ToReducedString()}' ({parameters}).");
|
||||
}
|
||||
|
||||
internal void Take()
|
||||
{
|
||||
var x = Screen.AllScreens.Min(s => s.Bounds.X);
|
||||
var y = Screen.AllScreens.Min(s => s.Bounds.Y);
|
||||
var width = Screen.AllScreens.Max(s => s.Bounds.X + s.Bounds.Width) - x;
|
||||
var height = Screen.AllScreens.Max(s => s.Bounds.Y + s.Bounds.Height) - y;
|
||||
|
||||
Bitmap = new Bitmap(width, height, PixelFormat.Format24bppRgb);
|
||||
Format = settings.ImageFormat;
|
||||
Height = height;
|
||||
Width = width;
|
||||
|
||||
using (var graphics = Graphics.FromImage(Bitmap))
|
||||
{
|
||||
graphics.CopyFromScreen(x, y, 0, 0, new Size(width, height), CopyPixelOperation.SourceCopy);
|
||||
graphics.DrawCursorPosition();
|
||||
}
|
||||
|
||||
Serialize();
|
||||
}
|
||||
|
||||
private void Downscale()
|
||||
{
|
||||
if (settings.ImageDownscaling > 1)
|
||||
{
|
||||
Height = Convert.ToInt32(Height / settings.ImageDownscaling);
|
||||
Width = Convert.ToInt32(Width / settings.ImageDownscaling);
|
||||
|
||||
var downscaled = new Bitmap(Width, Height, Bitmap.PixelFormat);
|
||||
|
||||
Bitmap.DrawInto(downscaled, new Rectangle(0, 0, Width, Height), ScalingMode.NearestNeighbor);
|
||||
Bitmap.Dispose();
|
||||
Bitmap = downscaled;
|
||||
}
|
||||
}
|
||||
|
||||
private void Quantize()
|
||||
{
|
||||
var ditherer = settings.ImageDownscaling > 1 ? OrderedDitherer.Bayer2x2 : default;
|
||||
var pixelFormat = settings.ImageQuantization.ToPixelFormat();
|
||||
var quantizer = settings.ImageQuantization.ToQuantizer();
|
||||
|
||||
Bitmap = Bitmap.ConvertPixelFormat(pixelFormat, quantizer, ditherer);
|
||||
}
|
||||
|
||||
private void Serialize()
|
||||
{
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
if (Format == ImageFormat.Jpg)
|
||||
{
|
||||
SerializeJpg(memoryStream);
|
||||
}
|
||||
else
|
||||
{
|
||||
Bitmap.Save(memoryStream, Format.ToSystemFormat());
|
||||
}
|
||||
|
||||
Data = memoryStream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private void SerializeJpg(MemoryStream memoryStream)
|
||||
{
|
||||
var codec = ImageCodecInfo.GetImageEncoders().First(c => c.FormatID == System.Drawing.Imaging.ImageFormat.Jpeg.Guid);
|
||||
var parameters = new EncoderParameters(1);
|
||||
var quality = 100;
|
||||
|
||||
parameters.Param[0] = new EncoderParameter(Encoder.Quality, quality);
|
||||
Bitmap.Save(memoryStream, codec, parameters);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Timers;
|
||||
using SafeExamBrowser.Core.Contracts.Notifications.Events;
|
||||
using SafeExamBrowser.Core.Contracts.Resources.Icons;
|
||||
using SafeExamBrowser.I18n.Contracts;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Service;
|
||||
using SafeExamBrowser.Server.Contracts.Events.Proctoring;
|
||||
using SafeExamBrowser.Settings.Proctoring;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
||||
{
|
||||
internal class ScreenProctoringImplementation : ProctoringImplementation
|
||||
{
|
||||
private readonly IModuleLogger logger;
|
||||
private readonly ServiceProxy service;
|
||||
private readonly ScreenProctoringSettings settings;
|
||||
private readonly IText text;
|
||||
private readonly Timer timer;
|
||||
|
||||
internal override string Name => nameof(ScreenProctoring);
|
||||
|
||||
public override event NotificationChangedEventHandler NotificationChanged;
|
||||
|
||||
internal ScreenProctoringImplementation(IModuleLogger logger, ServiceProxy service, ProctoringSettings settings, IText text)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.service = service;
|
||||
this.settings = settings.ScreenProctoring;
|
||||
this.text = text;
|
||||
this.timer = new Timer();
|
||||
}
|
||||
|
||||
internal override void Initialize()
|
||||
{
|
||||
var start = true;
|
||||
|
||||
start &= !string.IsNullOrWhiteSpace(settings.ClientId);
|
||||
start &= !string.IsNullOrWhiteSpace(settings.ClientSecret);
|
||||
start &= !string.IsNullOrWhiteSpace(settings.GroupId);
|
||||
start &= !string.IsNullOrWhiteSpace(settings.ServiceUrl);
|
||||
|
||||
timer.AutoReset = false;
|
||||
timer.Interval = settings.MaxInterval;
|
||||
|
||||
if (start)
|
||||
{
|
||||
logger.Info($"Initialized proctoring: All settings are valid, starting automatically...");
|
||||
|
||||
Connect();
|
||||
Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowNotificationInactive();
|
||||
|
||||
logger.Info($"Initialized proctoring: Not all settings are valid or a server session is active, not starting automatically.");
|
||||
}
|
||||
}
|
||||
|
||||
internal override void ProctoringConfigurationReceived(bool allowChat, bool receiveAudio, bool receiveVideo)
|
||||
{
|
||||
// Nothing to do here for now...
|
||||
}
|
||||
|
||||
internal override void ProctoringInstructionReceived(InstructionEventArgs args)
|
||||
{
|
||||
if (args is ScreenProctoringInstruction instruction)
|
||||
{
|
||||
logger.Info($"Proctoring instruction received: {instruction.Method}.");
|
||||
|
||||
if (instruction.Method == InstructionMethod.Join)
|
||||
{
|
||||
settings.ClientId = instruction.ClientId;
|
||||
settings.ClientSecret = instruction.ClientSecret;
|
||||
settings.GroupId = instruction.GroupId;
|
||||
settings.ServiceUrl = instruction.ServiceUrl;
|
||||
|
||||
Connect(instruction.SessionId);
|
||||
Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
logger.Info("Successfully processed instruction.");
|
||||
}
|
||||
}
|
||||
|
||||
internal override void Start()
|
||||
{
|
||||
timer.Elapsed += Timer_Elapsed;
|
||||
timer.Start();
|
||||
|
||||
ShowNotificationActive();
|
||||
|
||||
logger.Info($"Started proctoring.");
|
||||
}
|
||||
|
||||
internal override void Stop()
|
||||
{
|
||||
timer.Elapsed -= Timer_Elapsed;
|
||||
timer.Stop();
|
||||
|
||||
TerminateServiceSession();
|
||||
ShowNotificationInactive();
|
||||
|
||||
logger.Info("Stopped proctoring.");
|
||||
}
|
||||
|
||||
internal override void Terminate()
|
||||
{
|
||||
if (timer.Enabled)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
TerminateNotification();
|
||||
|
||||
logger.Info("Terminated proctoring.");
|
||||
}
|
||||
|
||||
protected override void ActivateNotification()
|
||||
{
|
||||
// Nothing to do here for now...
|
||||
}
|
||||
|
||||
protected override void TerminateNotification()
|
||||
{
|
||||
// Nothing to do here for now...
|
||||
}
|
||||
|
||||
private void Connect(string sessionId = default)
|
||||
{
|
||||
logger.Info("Connecting to service...");
|
||||
|
||||
var connect = service.Connect(settings.ServiceUrl);
|
||||
|
||||
if (connect.Success)
|
||||
{
|
||||
if (sessionId == default)
|
||||
{
|
||||
logger.Info("Creating session...");
|
||||
service.CreateSession(settings.GroupId);
|
||||
}
|
||||
else
|
||||
{
|
||||
service.SessionId = sessionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowNotificationActive()
|
||||
{
|
||||
// TODO: Replace with actual icon!
|
||||
IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/ProctoringNotification_Active.xaml") };
|
||||
Tooltip = text.Get(TextKey.Notification_ProctoringActiveTooltip);
|
||||
NotificationChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void ShowNotificationInactive()
|
||||
{
|
||||
// TODO: Replace with actual icon!
|
||||
IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/ProctoringNotification_Inactive.xaml") };
|
||||
Tooltip = text.Get(TextKey.Notification_ProctoringInactiveTooltip);
|
||||
NotificationChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void TerminateServiceSession()
|
||||
{
|
||||
if (service.IsConnected)
|
||||
{
|
||||
logger.Info("Terminating session...");
|
||||
service.TerminateSession();
|
||||
}
|
||||
}
|
||||
|
||||
private void Timer_Elapsed(object sender, ElapsedEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var screenShot = new ScreenShot(logger.CloneFor(nameof(ScreenShot)), settings))
|
||||
{
|
||||
screenShot.Take();
|
||||
screenShot.Compress();
|
||||
service.SendScreenShot(screenShot);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("Failed to process screen shot!", e);
|
||||
}
|
||||
|
||||
timer.Start();
|
||||
}
|
||||
}
|
||||
}
|
26
SafeExamBrowser.Proctoring/ScreenProctoring/Service/Api.cs
Normal file
26
SafeExamBrowser.Proctoring/ScreenProctoring/Service/Api.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service
|
||||
{
|
||||
internal class Api
|
||||
{
|
||||
internal const string SESSION_ID = "%%_SESSION_ID_%%";
|
||||
|
||||
internal string AccessTokenEndpoint { get; set; }
|
||||
internal string ScreenShotEndpoint { get; set; }
|
||||
internal string SessionEndpoint { get; set; }
|
||||
|
||||
internal Api()
|
||||
{
|
||||
AccessTokenEndpoint = "/oauth/token";
|
||||
ScreenShotEndpoint = $"/seb-api/v1/session/{SESSION_ID}/screenshot";
|
||||
SessionEndpoint = "/seb-api/v1/session";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service
|
||||
{
|
||||
internal class Parser
|
||||
{
|
||||
private readonly ILogger logger;
|
||||
|
||||
internal Parser(ILogger logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
internal bool IsTokenExpired(HttpContent content)
|
||||
{
|
||||
var isExpired = false;
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonConvert.DeserializeObject(Extract(content)) as JObject;
|
||||
var error = json["error"].Value<string>();
|
||||
|
||||
isExpired = error?.Equals("invalid_token", StringComparison.OrdinalIgnoreCase) == true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("Failed to parse token expiration content!", e);
|
||||
}
|
||||
|
||||
return isExpired;
|
||||
}
|
||||
|
||||
internal bool TryParseOauth2Token(HttpContent content, out string oauth2Token)
|
||||
{
|
||||
oauth2Token = default;
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonConvert.DeserializeObject(Extract(content)) as JObject;
|
||||
|
||||
oauth2Token = json["access_token"].Value<string>();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("Failed to parse Oauth2 token!", e);
|
||||
}
|
||||
|
||||
return oauth2Token != default;
|
||||
}
|
||||
|
||||
internal bool TryParseSessionId(HttpResponseMessage response, out string sessionId)
|
||||
{
|
||||
sessionId = default;
|
||||
|
||||
try
|
||||
{
|
||||
if (response.Headers.TryGetValues(Header.SESSION_ID, out var values))
|
||||
{
|
||||
sessionId = values.First();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("Failed to parse session identifier!", e);
|
||||
}
|
||||
|
||||
return sessionId != default;
|
||||
}
|
||||
|
||||
private string Extract(HttpContent content)
|
||||
{
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
return await content.ReadAsStreamAsync();
|
||||
});
|
||||
var stream = task.GetAwaiter().GetResult();
|
||||
var reader = new StreamReader(stream);
|
||||
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests
|
||||
{
|
||||
internal static class ContentType
|
||||
{
|
||||
internal const string JSON = "application/json;charset=UTF-8";
|
||||
internal const string OCTET_STREAM = "application/octet-stream";
|
||||
internal const string URL_ENCODED = "application/x-www-form-urlencoded";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
using System.Net.Http;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests
|
||||
{
|
||||
internal class CreateSessionRequest : Request
|
||||
{
|
||||
internal CreateSessionRequest(Api api, HttpClient httpClient, ILogger logger, Parser parser) : base(api, httpClient, logger, parser)
|
||||
{
|
||||
}
|
||||
|
||||
internal bool TryExecute(string groupId, out string message, out string sessionId)
|
||||
{
|
||||
var group = (Header.GROUP_ID, groupId);
|
||||
var success = TryExecute(HttpMethod.Post, api.SessionEndpoint, out var response, string.Empty, ContentType.URL_ENCODED, Authorization, group);
|
||||
|
||||
message = response.ToLogString();
|
||||
sessionId = default;
|
||||
|
||||
if (success)
|
||||
{
|
||||
parser.TryParseSessionId(response, out sessionId);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests
|
||||
{
|
||||
internal static class Extensions
|
||||
{
|
||||
internal static string ToLogString(this HttpResponseMessage response)
|
||||
{
|
||||
return $"{(int?) response?.StatusCode} {response?.StatusCode} {response?.ReasonPhrase}";
|
||||
}
|
||||
|
||||
internal static long ToUnixTimestamp(this DateTime date)
|
||||
{
|
||||
return new DateTimeOffset(date).ToUnixTimeMilliseconds();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests
|
||||
{
|
||||
internal static class Header
|
||||
{
|
||||
internal const string ACCEPT = "Accept";
|
||||
internal const string AUTHORIZATION = "Authorization";
|
||||
internal const string GROUP_ID = "SEB_GROUP_UUID";
|
||||
internal const string SESSION_ID = "SEB_SESSION_UUID";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
using System.Net.Http;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests
|
||||
{
|
||||
internal class OAuth2TokenRequest : Request
|
||||
{
|
||||
internal OAuth2TokenRequest(Api api, HttpClient httpClient, ILogger logger, Parser parser) : base(api, httpClient, logger, parser)
|
||||
{
|
||||
}
|
||||
|
||||
internal bool TryExecute(out string message)
|
||||
{
|
||||
return TryRetrieveOAuth2Token(out message);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests
|
||||
{
|
||||
internal abstract class Request
|
||||
{
|
||||
private const int ATTEMPTS = 5;
|
||||
|
||||
private static string connectionToken;
|
||||
private static string oauth2Token;
|
||||
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
protected readonly Api api;
|
||||
protected readonly ILogger logger;
|
||||
protected readonly Parser parser;
|
||||
|
||||
protected (string, string) Authorization => (Header.AUTHORIZATION, $"Bearer {oauth2Token}");
|
||||
|
||||
internal static string ConnectionToken
|
||||
{
|
||||
get { return connectionToken; }
|
||||
set { connectionToken = value; }
|
||||
}
|
||||
|
||||
internal static string Oauth2Token
|
||||
{
|
||||
get { return oauth2Token; }
|
||||
set { oauth2Token = value; }
|
||||
}
|
||||
|
||||
protected Request(Api api, HttpClient httpClient, ILogger logger, Parser parser)
|
||||
{
|
||||
this.api = api;
|
||||
this.httpClient = httpClient;
|
||||
this.logger = logger;
|
||||
this.parser = parser;
|
||||
}
|
||||
|
||||
protected bool TryExecute(
|
||||
HttpMethod method,
|
||||
string url,
|
||||
out HttpResponseMessage response,
|
||||
object content = default,
|
||||
string contentType = default,
|
||||
params (string name, string value)[] headers)
|
||||
{
|
||||
response = default;
|
||||
|
||||
for (var attempt = 0; attempt < ATTEMPTS && (response == default || !response.IsSuccessStatusCode); attempt++)
|
||||
{
|
||||
var request = BuildRequest(method, url, content, contentType, headers);
|
||||
|
||||
try
|
||||
{
|
||||
response = httpClient.SendAsync(request).GetAwaiter().GetResult();
|
||||
|
||||
logger.Debug($"Completed request: {request.Method} '{request.RequestUri}' -> {response.ToLogString()}");
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized && parser.IsTokenExpired(response.Content))
|
||||
{
|
||||
logger.Info("OAuth2 token has expired, attempting to retrieve new one...");
|
||||
|
||||
if (TryRetrieveOAuth2Token(out var message))
|
||||
{
|
||||
headers = UpdateOAuth2Token(headers);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
logger.Error($"Request {request.Method} '{request.RequestUri}' did not complete within {httpClient.Timeout}ms!");
|
||||
break;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error($"Request {request.Method} '{request.RequestUri}' has failed!", e);
|
||||
}
|
||||
}
|
||||
|
||||
return response != default && response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
protected bool TryRetrieveOAuth2Token(out string message)
|
||||
{
|
||||
var secret = Convert.ToBase64String(Encoding.UTF8.GetBytes("test:test"));
|
||||
var authorization = (Header.AUTHORIZATION, $"Basic {secret}");
|
||||
var content = "grant_type=client_credentials&scope=read write";
|
||||
var success = TryExecute(HttpMethod.Post, api.AccessTokenEndpoint, out var response, content, ContentType.URL_ENCODED, authorization);
|
||||
|
||||
message = response.ToLogString();
|
||||
|
||||
if (success && parser.TryParseOauth2Token(response.Content, out oauth2Token))
|
||||
{
|
||||
logger.Info("Successfully retrieved OAuth2 token.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error("Failed to retrieve OAuth2 token!");
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private HttpRequestMessage BuildRequest(
|
||||
HttpMethod method,
|
||||
string url,
|
||||
object content = default,
|
||||
string contentType = default,
|
||||
params (string name, string value)[] headers)
|
||||
{
|
||||
var request = new HttpRequestMessage(method, url);
|
||||
|
||||
if (content != default)
|
||||
{
|
||||
if (content is string)
|
||||
{
|
||||
request.Content = new StringContent(content as string, Encoding.UTF8);
|
||||
}
|
||||
|
||||
if (content is byte[])
|
||||
{
|
||||
request.Content = new ByteArrayContent(content as byte[]);
|
||||
}
|
||||
|
||||
if (contentType != default)
|
||||
{
|
||||
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
|
||||
}
|
||||
}
|
||||
|
||||
request.Headers.Add(Header.ACCEPT, "application/json, */*");
|
||||
|
||||
foreach (var (name, value) in headers)
|
||||
{
|
||||
request.Headers.Add(name, value);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private (string name, string value)[] UpdateOAuth2Token((string name, string value)[] headers)
|
||||
{
|
||||
var result = new List<(string name, string value)>();
|
||||
|
||||
foreach (var header in headers)
|
||||
{
|
||||
if (header.name == Header.AUTHORIZATION)
|
||||
{
|
||||
result.Add((Header.AUTHORIZATION, $"Bearer {oauth2Token}"));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(header);
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging;
|
||||
using SafeExamBrowser.Settings.Proctoring;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests
|
||||
{
|
||||
internal class ScreenShotRequest : Request
|
||||
{
|
||||
internal ScreenShotRequest(Api api, HttpClient httpClient, ILogger logger, Parser parser) : base(api, httpClient, logger, parser)
|
||||
{
|
||||
}
|
||||
|
||||
internal bool TryExecute(ScreenShot screenShot, string sessionId, out string message)
|
||||
{
|
||||
var imageFormat = ("imageFormat", ToString(screenShot.Format));
|
||||
var timestamp = ("timestamp", DateTime.Now.ToUnixTimestamp().ToString());
|
||||
var url = api.ScreenShotEndpoint.Replace(Api.SESSION_ID, sessionId);
|
||||
var success = TryExecute(HttpMethod.Post, url, out var response, screenShot.Data, ContentType.OCTET_STREAM, Authorization, imageFormat, timestamp);
|
||||
|
||||
message = response.ToLogString();
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private string ToString(ImageFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case ImageFormat.Bmp:
|
||||
return "bmp";
|
||||
case ImageFormat.Gif:
|
||||
return "gif";
|
||||
case ImageFormat.Jpg:
|
||||
return "jpg";
|
||||
case ImageFormat.Png:
|
||||
return "png";
|
||||
default:
|
||||
throw new NotImplementedException($"Image format {format} is not yet implemented!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
using System.Net.Http;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests
|
||||
{
|
||||
internal class TerminateSessionRequest : Request
|
||||
{
|
||||
internal TerminateSessionRequest(Api api, HttpClient httpClient, ILogger logger, Parser parser) : base(api, httpClient, logger, parser)
|
||||
{
|
||||
}
|
||||
|
||||
internal bool TryExecute(string sessionId, out string message)
|
||||
{
|
||||
var url = $"{api.SessionEndpoint}/{sessionId}";
|
||||
var success = TryExecute(HttpMethod.Delete, url, out var response, contentType: ContentType.URL_ENCODED, headers: Authorization);
|
||||
|
||||
message = response.ToLogString();
|
||||
|
||||
return success;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service
|
||||
{
|
||||
internal class ServiceProxy
|
||||
{
|
||||
private readonly Api api;
|
||||
private readonly ILogger logger;
|
||||
private readonly Parser parser;
|
||||
|
||||
private HttpClient httpClient;
|
||||
|
||||
internal bool IsConnected => SessionId != default;
|
||||
internal string SessionId { get; set; }
|
||||
|
||||
internal ServiceProxy(ILogger logger)
|
||||
{
|
||||
this.api = new Api();
|
||||
this.logger = logger;
|
||||
this.parser = new Parser(logger);
|
||||
}
|
||||
|
||||
internal ServiceResponse Connect(string serviceUrl)
|
||||
{
|
||||
httpClient = new HttpClient { BaseAddress = new Uri(serviceUrl) };
|
||||
|
||||
var request = new OAuth2TokenRequest(api, httpClient, logger, parser);
|
||||
var success = request.TryExecute(out var message);
|
||||
|
||||
if (success)
|
||||
{
|
||||
logger.Info("Successfully connected to service.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error("Failed to connect to service!");
|
||||
}
|
||||
|
||||
return new ServiceResponse(success, message);
|
||||
}
|
||||
|
||||
internal ServiceResponse CreateSession(string groupId)
|
||||
{
|
||||
var request = new CreateSessionRequest(api, httpClient, logger, parser);
|
||||
var success = request.TryExecute(groupId, out var message, out var sessionId);
|
||||
|
||||
if (success)
|
||||
{
|
||||
SessionId = sessionId;
|
||||
logger.Info("Successfully created session.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error("Failed to create session!");
|
||||
}
|
||||
|
||||
return new ServiceResponse(success, message);
|
||||
}
|
||||
|
||||
internal ServiceResponse SendScreenShot(ScreenShot screenShot)
|
||||
{
|
||||
var request = new ScreenShotRequest(api, httpClient, logger, parser);
|
||||
var success = request.TryExecute(screenShot, SessionId, out var message);
|
||||
|
||||
if (success)
|
||||
{
|
||||
logger.Info($"Successfully sent screen shot ({screenShot}).");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error("Failed to send screen shot!");
|
||||
}
|
||||
|
||||
return new ServiceResponse(success, message);
|
||||
}
|
||||
|
||||
internal ServiceResponse TerminateSession()
|
||||
{
|
||||
var request = new TerminateSessionRequest(api, httpClient, logger, parser);
|
||||
var success = request.TryExecute(SessionId, out var message);
|
||||
|
||||
if (success)
|
||||
{
|
||||
SessionId = default;
|
||||
logger.Info("Successfully terminated session.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error("Failed to terminate session!");
|
||||
}
|
||||
|
||||
return new ServiceResponse(success, message);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service
|
||||
{
|
||||
internal class ServiceResponse
|
||||
{
|
||||
internal string Message { get; }
|
||||
internal bool Success { get; }
|
||||
|
||||
internal ServiceResponse(bool success, string message = default)
|
||||
{
|
||||
Message = message;
|
||||
Success = success;
|
||||
}
|
||||
}
|
||||
|
||||
internal class ServiceResponse<T> : ServiceResponse
|
||||
{
|
||||
internal T Value { get; }
|
||||
|
||||
internal ServiceResponse(bool success, T value, string message = default) : base(success, message)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="KGySoft.CoreLibraries" version="8.0.0" targetFramework="net48" />
|
||||
<package id="KGySoft.Drawing" version="8.0.0" targetFramework="net48" />
|
||||
<package id="KGySoft.Drawing.Core" version="8.0.0" targetFramework="net48" />
|
||||
<package id="Microsoft.Web.WebView2" version="1.0.2088.41" targetFramework="net48" />
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
|
||||
</packages>
|
|
@ -31,9 +31,14 @@ namespace SafeExamBrowser.Runtime.Operations
|
|||
{
|
||||
var result = OperationResult.Success;
|
||||
|
||||
if (Context.Next.Settings.Proctoring.Enabled)
|
||||
if (Context.Next.Settings.Proctoring.JitsiMeet.Enabled)
|
||||
{
|
||||
result = ShowDisclaimer();
|
||||
result = ShowVideoProctoringDisclaimer();
|
||||
}
|
||||
else if (Context.Next.Settings.Proctoring.ScreenProctoring.Enabled)
|
||||
{
|
||||
// TODO: Implement disclaimer!
|
||||
// result = ShowScreenProctoringDisclaimer();
|
||||
}
|
||||
else if (Context.Next.Settings.Proctoring.Zoom.Enabled)
|
||||
{
|
||||
|
@ -51,9 +56,14 @@ namespace SafeExamBrowser.Runtime.Operations
|
|||
{
|
||||
var result = OperationResult.Success;
|
||||
|
||||
if (Context.Next.Settings.Proctoring.Enabled)
|
||||
if (Context.Next.Settings.Proctoring.JitsiMeet.Enabled)
|
||||
{
|
||||
result = ShowDisclaimer();
|
||||
result = ShowVideoProctoringDisclaimer();
|
||||
}
|
||||
else if (Context.Next.Settings.Proctoring.ScreenProctoring.Enabled)
|
||||
{
|
||||
// TODO: Implement disclaimer!
|
||||
// result = ShowScreenProctoringDisclaimer();
|
||||
}
|
||||
else if (Context.Next.Settings.Proctoring.Zoom.Enabled)
|
||||
{
|
||||
|
@ -72,7 +82,7 @@ namespace SafeExamBrowser.Runtime.Operations
|
|||
return OperationResult.Success;
|
||||
}
|
||||
|
||||
private OperationResult ShowDisclaimer()
|
||||
private OperationResult ShowVideoProctoringDisclaimer()
|
||||
{
|
||||
var args = new MessageEventArgs
|
||||
{
|
||||
|
|
|
@ -27,7 +27,7 @@ namespace SafeExamBrowser.Runtime.Operations
|
|||
|
||||
public override OperationResult Perform()
|
||||
{
|
||||
if (Context.Next.Settings.Proctoring.Enabled && Context.Next.Settings.Security.KioskMode == KioskMode.CreateNewDesktop)
|
||||
if (Context.Next.Settings.Proctoring.JitsiMeet.Enabled && Context.Next.Settings.Security.KioskMode == KioskMode.CreateNewDesktop)
|
||||
{
|
||||
Context.Next.Settings.Security.KioskMode = KioskMode.DisableExplorerShell;
|
||||
logger.Info("Switched kiosk mode to Disable Explorer Shell due to remote proctoring being enabled.");
|
||||
|
|
|
@ -23,7 +23,7 @@ namespace SafeExamBrowser.Server.Contracts.Data
|
|||
/// </summary>
|
||||
public bool Success { get; }
|
||||
|
||||
public ServerResponse(bool success, string message = default(string))
|
||||
public ServerResponse(bool success, string message = default)
|
||||
{
|
||||
Message = message;
|
||||
Success = success;
|
||||
|
@ -41,7 +41,7 @@ namespace SafeExamBrowser.Server.Contracts.Data
|
|||
/// </summary>
|
||||
public T Value { get; }
|
||||
|
||||
public ServerResponse(bool success, T value, string message = default(string)) : base(success, message)
|
||||
public ServerResponse(bool success, T value, string message = default) : base(success, message)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Server.Contracts.Events.Proctoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines all parameters for a proctoring instruction received by the <see cref="IServerProxy"/>.
|
||||
/// </summary>
|
||||
public abstract class InstructionEventArgs
|
||||
{
|
||||
public InstructionMethod Method { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Server.Contracts.Events.Proctoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines all possible methods for a proctoring instruction.
|
||||
/// </summary>
|
||||
public enum InstructionMethod
|
||||
{
|
||||
/// <summary>
|
||||
/// Instructs to start proctoring resp. join a proctoring event or session.
|
||||
/// </summary>
|
||||
Join,
|
||||
|
||||
/// <summary>
|
||||
/// Instructs to stop proctoring resp. leave a proctoring event or session.
|
||||
/// </summary>
|
||||
Leave
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Server.Contracts.Events.Proctoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the parameters of a proctoring instruction for provider Jitsi Meet.
|
||||
/// </summary>
|
||||
public class JitsiMeetInstruction : InstructionEventArgs
|
||||
{
|
||||
public string RoomName { get; set; }
|
||||
public string ServerUrl { get; set; }
|
||||
public string Token { get; set; }
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Server.Contracts.Events
|
||||
namespace SafeExamBrowser.Server.Contracts.Events.Proctoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Event handler used to indicate that proctoring configuration data has been received.
|
|
@ -6,10 +6,10 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Server.Contracts.Events
|
||||
namespace SafeExamBrowser.Server.Contracts.Events.Proctoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Event handler used to indicate that a proctoring instruction has been received.
|
||||
/// </summary>
|
||||
public delegate void ProctoringInstructionReceivedEventHandler(ProctoringInstructionEventArgs args);
|
||||
public delegate void ProctoringInstructionReceivedEventHandler(InstructionEventArgs args);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Server.Contracts.Events.Proctoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the parameters of a proctoring instruction for the screen proctoring implementation.
|
||||
/// </summary>
|
||||
public class ScreenProctoringInstruction : InstructionEventArgs
|
||||
{
|
||||
public string ClientId { get; set; }
|
||||
public string ClientSecret { get; set; }
|
||||
public string GroupId { get; set; }
|
||||
public string ServiceUrl { get; set; }
|
||||
public string SessionId { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Server.Contracts.Events.Proctoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the parameters of a proctoring instruction for provider Zoom.
|
||||
/// </summary>
|
||||
public class ZoomInstruction : InstructionEventArgs
|
||||
{
|
||||
public string MeetingNumber { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string SdkKey { get; set; }
|
||||
public string Signature { get; set; }
|
||||
public string Subject { get; set; }
|
||||
public string UserName { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Server.Contracts.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines all parameters for a proctoring instruction received by the <see cref="IServerProxy"/>.
|
||||
/// </summary>
|
||||
public class ProctoringInstructionEventArgs
|
||||
{
|
||||
public string JitsiMeetRoomName { get; set; }
|
||||
public string JitsiMeetServerUrl { get; set; }
|
||||
public string JitsiMeetToken { get; set; }
|
||||
public string ZoomMeetingNumber { get; set; }
|
||||
public string ZoomPassword { get; set; }
|
||||
public string ZoomSdkKey { get; set; }
|
||||
public string ZoomSignature { get; set; }
|
||||
public string ZoomSubject { get; set; }
|
||||
public string ZoomUserName { get; set; }
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using SafeExamBrowser.Server.Contracts.Data;
|
||||
using SafeExamBrowser.Server.Contracts.Events;
|
||||
using SafeExamBrowser.Server.Contracts.Events.Proctoring;
|
||||
using SafeExamBrowser.Settings.Server;
|
||||
|
||||
namespace SafeExamBrowser.Server.Contracts
|
||||
|
|
|
@ -58,9 +58,13 @@
|
|||
<Compile Include="Data\ConnectionInfo.cs" />
|
||||
<Compile Include="Data\Exam.cs" />
|
||||
<Compile Include="Events\LockScreenRequestedEventHandler.cs" />
|
||||
<Compile Include="Events\ProctoringConfigurationReceivedEventHandler.cs" />
|
||||
<Compile Include="Events\ProctoringInstructionEventArgs.cs" />
|
||||
<Compile Include="Events\ProctoringInstructionReceivedEventHandler.cs" />
|
||||
<Compile Include="Events\Proctoring\InstructionMethod.cs" />
|
||||
<Compile Include="Events\Proctoring\JitsiMeetInstruction.cs" />
|
||||
<Compile Include="Events\Proctoring\ProctoringConfigurationReceivedEventHandler.cs" />
|
||||
<Compile Include="Events\Proctoring\InstructionEventArgs.cs" />
|
||||
<Compile Include="Events\Proctoring\ProctoringInstructionReceivedEventHandler.cs" />
|
||||
<Compile Include="Events\Proctoring\ScreenProctoringInstruction.cs" />
|
||||
<Compile Include="Events\Proctoring\ZoomInstruction.cs" />
|
||||
<Compile Include="Events\ServerEventHandler.cs" />
|
||||
<Compile Include="Events\TerminationRequestedEventHandler.cs" />
|
||||
<Compile Include="IServerProxy.cs" />
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
using SafeExamBrowser.Server.Contracts.Events;
|
||||
using SafeExamBrowser.Server.Contracts.Events.Proctoring;
|
||||
|
||||
namespace SafeExamBrowser.Server.Data
|
||||
{
|
||||
|
@ -14,15 +14,10 @@ namespace SafeExamBrowser.Server.Data
|
|||
{
|
||||
internal bool AllowChat { get; set; }
|
||||
internal int Id { get; set; }
|
||||
internal ProctoringInstructionEventArgs Instruction { get; set; }
|
||||
internal InstructionEventArgs Instruction { get; set; }
|
||||
internal string Message { get; set; }
|
||||
internal bool ReceiveAudio { get; set; }
|
||||
internal bool ReceiveVideo { get; set; }
|
||||
internal AttributeType Type { get; set; }
|
||||
|
||||
internal Attributes()
|
||||
{
|
||||
Instruction = new ProctoringInstructionEventArgs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ using Newtonsoft.Json;
|
|||
using Newtonsoft.Json.Linq;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
using SafeExamBrowser.Server.Contracts.Data;
|
||||
using SafeExamBrowser.Server.Contracts.Events.Proctoring;
|
||||
using SafeExamBrowser.Server.Data;
|
||||
using SafeExamBrowser.Server.Requests;
|
||||
|
||||
|
@ -314,19 +315,55 @@ namespace SafeExamBrowser.Server
|
|||
switch (provider)
|
||||
{
|
||||
case "JITSI_MEET":
|
||||
attributes.Instruction.JitsiMeetRoomName = attributesJson["jitsiMeetRoom"].Value<string>();
|
||||
attributes.Instruction.JitsiMeetServerUrl = attributesJson["jitsiMeetServerURL"].Value<string>();
|
||||
attributes.Instruction.JitsiMeetToken = attributesJson["jitsiMeetToken"].Value<string>();
|
||||
attributes.Instruction = ParseJitsiMeetInstruction(attributesJson);
|
||||
break;
|
||||
case "SCREEN_PROCTORING":
|
||||
attributes.Instruction = ParseScreenProctoringInstruction(attributesJson);
|
||||
break;
|
||||
case "ZOOM":
|
||||
attributes.Instruction.ZoomMeetingNumber = attributesJson["zoomRoom"].Value<string>();
|
||||
attributes.Instruction.ZoomPassword = attributesJson["zoomMeetingKey"].Value<string>();
|
||||
attributes.Instruction.ZoomSdkKey = attributesJson["zoomAPIKey"].Value<string>();
|
||||
attributes.Instruction.ZoomSignature = attributesJson["zoomToken"].Value<string>();
|
||||
attributes.Instruction.ZoomSubject = attributesJson["zoomSubject"].Value<string>();
|
||||
attributes.Instruction.ZoomUserName = attributesJson["zoomUserName"].Value<string>();
|
||||
attributes.Instruction = ParseZoomInstruction(attributesJson);
|
||||
break;
|
||||
}
|
||||
|
||||
if (attributes.Instruction != default)
|
||||
{
|
||||
attributes.Instruction.Method = attributesJson["method"].Value<string>() == "JOIN" ? InstructionMethod.Join : InstructionMethod.Leave;
|
||||
}
|
||||
}
|
||||
|
||||
private JitsiMeetInstruction ParseJitsiMeetInstruction(JObject attributesJson)
|
||||
{
|
||||
return new JitsiMeetInstruction
|
||||
{
|
||||
RoomName = attributesJson["jitsiMeetRoom"].Value<string>(),
|
||||
ServerUrl = attributesJson["jitsiMeetServerURL"].Value<string>(),
|
||||
Token = attributesJson["jitsiMeetToken"].Value<string>()
|
||||
};
|
||||
}
|
||||
|
||||
private ScreenProctoringInstruction ParseScreenProctoringInstruction(JObject attributesJson)
|
||||
{
|
||||
return new ScreenProctoringInstruction
|
||||
{
|
||||
ClientId = attributesJson["screenProctoringClientId"].Value<string>(),
|
||||
ClientSecret = attributesJson["screenProctoringClientSecret"].Value<string>(),
|
||||
GroupId = attributesJson["screenProctoringGroupId"].Value<string>(),
|
||||
ServiceUrl = attributesJson["screenProctoringServiceURL"].Value<string>(),
|
||||
SessionId = attributesJson["screenProctoringClientSessionId"].Value<string>()
|
||||
};
|
||||
}
|
||||
|
||||
private ZoomInstruction ParseZoomInstruction(JObject attributesJson)
|
||||
{
|
||||
return new ZoomInstruction
|
||||
{
|
||||
MeetingNumber = attributesJson["zoomRoom"].Value<string>(),
|
||||
Password = attributesJson["zoomMeetingKey"].Value<string>(),
|
||||
SdkKey = attributesJson["zoomAPIKey"].Value<string>(),
|
||||
Signature = attributesJson["zoomToken"].Value<string>(),
|
||||
Subject = attributesJson["zoomSubject"].Value<string>(),
|
||||
UserName = attributesJson["zoomUserName"].Value<string>()
|
||||
};
|
||||
}
|
||||
|
||||
private void ParseReconfigurationInstruction(Attributes attributes, JObject attributesJson)
|
||||
|
|
|
@ -20,6 +20,7 @@ using SafeExamBrowser.Logging.Contracts;
|
|||
using SafeExamBrowser.Server.Contracts;
|
||||
using SafeExamBrowser.Server.Contracts.Data;
|
||||
using SafeExamBrowser.Server.Contracts.Events;
|
||||
using SafeExamBrowser.Server.Contracts.Events.Proctoring;
|
||||
using SafeExamBrowser.Server.Data;
|
||||
using SafeExamBrowser.Server.Requests;
|
||||
using SafeExamBrowser.Settings.Server;
|
||||
|
|
36
SafeExamBrowser.Settings/Proctoring/ImageFormat.cs
Normal file
36
SafeExamBrowser.Settings/Proctoring/ImageFormat.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Settings.Proctoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines all possible image formats for the screen proctoring.
|
||||
/// </summary>
|
||||
public enum ImageFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// An image with the Windows Bitmap format.
|
||||
/// </summary>
|
||||
Bmp,
|
||||
|
||||
/// <summary>
|
||||
/// An image with the Graphics Interchange Format format.
|
||||
/// </summary>
|
||||
Gif,
|
||||
|
||||
/// <summary>
|
||||
/// An image with the Joint Photographic Experts Group format.
|
||||
/// </summary>
|
||||
Jpg,
|
||||
|
||||
/// <summary>
|
||||
/// An image with the Portable Network Graphics format.
|
||||
/// </summary>
|
||||
Png
|
||||
}
|
||||
}
|
51
SafeExamBrowser.Settings/Proctoring/ImageQuantization.cs
Normal file
51
SafeExamBrowser.Settings/Proctoring/ImageQuantization.cs
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Settings.Proctoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines all possible image quantization algorithms for the screen proctoring.
|
||||
/// </summary>
|
||||
public enum ImageQuantization
|
||||
{
|
||||
/// <summary>
|
||||
/// Reduces an image to a black and white image with 1 bit per pixel.
|
||||
/// </summary>
|
||||
BlackAndWhite1bpp,
|
||||
|
||||
/// <summary>
|
||||
/// Reduces an image to a colored image with 8 bits per pixel (256 colors).
|
||||
/// </summary>
|
||||
Color8bpp,
|
||||
|
||||
/// <summary>
|
||||
/// Reduces an image to a colored image with 16 bits per pixel (65'536 colors).
|
||||
/// </summary>
|
||||
Color16bpp,
|
||||
|
||||
/// <summary>
|
||||
/// Reduces an image to a colored image with 24 bits per pixel (16'777'216 colors).
|
||||
/// </summary>
|
||||
Color24bpp,
|
||||
|
||||
/// <summary>
|
||||
/// Reduces an image to a grayscale image with 2 bits per pixel (4 shades).
|
||||
/// </summary>
|
||||
Grayscale2bpp,
|
||||
|
||||
/// <summary>
|
||||
/// Reduces an image to a grayscale image with 4 bits per pixel (16 shades).
|
||||
/// </summary>
|
||||
Grayscale4bpp,
|
||||
|
||||
/// <summary>
|
||||
/// Reduces an image to a grayscale image with 8 bits per pixel (256 shades).
|
||||
/// </summary>
|
||||
Grayscale8bpp
|
||||
}
|
||||
}
|
|
@ -31,6 +31,11 @@ namespace SafeExamBrowser.Settings.Proctoring
|
|||
/// </summary>
|
||||
public JitsiMeetSettings JitsiMeet { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// All settings for the screen proctoring.
|
||||
/// </summary>
|
||||
public ScreenProctoringSettings ScreenProctoring { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the raise hand notification will be shown in the shell.
|
||||
/// </summary>
|
||||
|
@ -54,6 +59,7 @@ namespace SafeExamBrowser.Settings.Proctoring
|
|||
public ProctoringSettings()
|
||||
{
|
||||
JitsiMeet = new JitsiMeetSettings();
|
||||
ScreenProctoring = new ScreenProctoringSettings();
|
||||
Zoom = new ZoomSettings();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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/.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace SafeExamBrowser.Settings.Proctoring
|
||||
{
|
||||
/// <summary>
|
||||
/// All settings for the screen proctoring.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ScreenProctoringSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the name of the active application shall be captured and transmitted as part of the image meta data.
|
||||
/// </summary>
|
||||
public bool CaptureApplicationName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the URL of the currently opened web page shall be captured and transmitted as part of the image meta data.
|
||||
/// </summary>
|
||||
public bool CaptureBrowserUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the title of the currently active window shall be captured and transmitted as part of the image meta data.
|
||||
/// </summary>
|
||||
public bool CaptureWindowTitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The client identifier used for authentication with the screen proctoring service.
|
||||
/// </summary>
|
||||
public string ClientId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The client secret used for authentication with the screen proctoring service.
|
||||
/// </summary>
|
||||
public string ClientSecret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the screen proctoring is enabled.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The identifier of the group to which the user belongs.
|
||||
/// </summary>
|
||||
public string GroupId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines the factor to be used for downscaling of the screen shots.
|
||||
/// </summary>
|
||||
public double ImageDownscaling { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines the image format to be used for the screen shots.
|
||||
/// </summary>
|
||||
public ImageFormat ImageFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines the algorithm to be used for quantization of the screen shots.
|
||||
/// </summary>
|
||||
public ImageQuantization ImageQuantization { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum time interval in milliseconds between screen shot transmissions.
|
||||
/// </summary>
|
||||
public int MaxInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The minimum time interval in milliseconds between screen shot transmissions.
|
||||
/// </summary>
|
||||
public int MinInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The URL of the screen proctoring service.
|
||||
/// </summary>
|
||||
public string ServiceUrl { get; set; }
|
||||
}
|
||||
}
|
|
@ -73,8 +73,11 @@
|
|||
<Compile Include="Browser\Proxy\ProxyConfiguration.cs" />
|
||||
<Compile Include="ConfigurationMode.cs" />
|
||||
<Compile Include="Monitoring\DisplaySettings.cs" />
|
||||
<Compile Include="Proctoring\ImageFormat.cs" />
|
||||
<Compile Include="Proctoring\ImageQuantization.cs" />
|
||||
<Compile Include="Proctoring\JitsiMeetSettings.cs" />
|
||||
<Compile Include="Proctoring\ProctoringSettings.cs" />
|
||||
<Compile Include="Proctoring\ScreenProctoringSettings.cs" />
|
||||
<Compile Include="Proctoring\WindowVisibility.cs" />
|
||||
<Compile Include="Proctoring\ZoomSettings.cs" />
|
||||
<Compile Include="Security\ClipboardPolicy.cs" />
|
||||
|
|
Loading…
Reference in a new issue