diff --git a/SafeExamBrowser.Client/CompositionRoot.cs b/SafeExamBrowser.Client/CompositionRoot.cs
index 2ff52fbf..3c0b045e 100644
--- a/SafeExamBrowser.Client/CompositionRoot.cs
+++ b/SafeExamBrowser.Client/CompositionRoot.cs
@@ -249,7 +249,7 @@ namespace SafeExamBrowser.Client
 
 		private IOperation BuildProctoringOperation()
 		{
-			var controller = new ProctoringController(uiFactory);
+			var controller = new ProctoringController(ModuleLogger(nameof(ProctoringController)), uiFactory);
 			var operation = new ProctoringOperation(context, logger, controller);
 
 			context.ProctoringController = controller;
diff --git a/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/ProctoringDataMapper.cs b/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/ProctoringDataMapper.cs
index 23097435..0627fea0 100644
--- a/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/ProctoringDataMapper.cs
+++ b/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/ProctoringDataMapper.cs
@@ -17,7 +17,50 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping
 		{
 			switch (key)
 			{
-				// TODO
+				case Keys.Proctoring.JitsiMeet.RoomName:
+					MapJitsiMeetRoomName(settings, value);
+					break;
+				case Keys.Proctoring.JitsiMeet.ServerUrl:
+					MapJitsiMeetServerUrl(settings, value);
+					break;
+				case Keys.Proctoring.JitsiMeet.Subject:
+					MapJitsiMeetSubject(settings, value);
+					break;
+				case Keys.Proctoring.JitsiMeet.Token:
+					MapJitsiMeetToken(settings, value);
+					break;
+			}
+		}
+
+		private void MapJitsiMeetRoomName(AppSettings settings, object value)
+		{
+			if (value is string name)
+			{
+				settings.Proctoring.JitsiMeet.RoomName = name;
+			}
+		}
+
+		private void MapJitsiMeetServerUrl(AppSettings settings, object value)
+		{
+			if (value is string url)
+			{
+				settings.Proctoring.JitsiMeet.ServerUrl = url;
+			}
+		}
+
+		private void MapJitsiMeetSubject(AppSettings settings, object value)
+		{
+			if (value is string subject)
+			{
+				settings.Proctoring.JitsiMeet.Subject = subject;
+			}
+		}
+
+		private void MapJitsiMeetToken(AppSettings settings, object value)
+		{
+			if (value is string token)
+			{
+				settings.Proctoring.JitsiMeet.Token = token;
 			}
 		}
 
diff --git a/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs b/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs
index c9b9d4de..899f464f 100644
--- a/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs
+++ b/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs
@@ -219,6 +219,10 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
 			internal static class JitsiMeet
 			{
 				internal const string Enabled = "jitsiMeetEnable";
+				internal const string RoomName = "jitsiMeetRoom";
+				internal const string ServerUrl = "jitsiMeetServerURL";
+				internal const string Subject = "jitsiMeetSubject";
+				internal const string Token = "jitsiMeetToken";
 			}
 
 			internal static class Zoom
diff --git a/SafeExamBrowser.Proctoring/JitsiMeet/index.html b/SafeExamBrowser.Proctoring/JitsiMeet/index.html
new file mode 100644
index 00000000..5fc8af18
--- /dev/null
+++ b/SafeExamBrowser.Proctoring/JitsiMeet/index.html
@@ -0,0 +1,22 @@
+<html>
+    <head>
+        <meta charset="utf-8" />
+    </head>
+    <body>
+        <div id="placeholder" />
+        <script src='https://meet.jit.si/external_api.js'></script>
+        <script type="text/javascript">
+            var domain = "%%_DOMAIN_%%";
+            var options = {
+                height: "100%",
+                jwt: "%%_TOKEN_%%",
+                parentNode: document.querySelector('#placeholder'),
+                roomName: "%%_ROOM_NAME_%%",
+                width: "100%"
+            };
+            var api = new JitsiMeetExternalAPI(domain, options);
+
+            api.executeCommand("subject", "%%_SUBJECT_%%");
+        </script>
+    </body>
+</html>
\ No newline at end of file
diff --git a/SafeExamBrowser.Proctoring/ProctoringControl.cs b/SafeExamBrowser.Proctoring/ProctoringControl.cs
index d9600a8d..bc4cfb68 100644
--- a/SafeExamBrowser.Proctoring/ProctoringControl.cs
+++ b/SafeExamBrowser.Proctoring/ProctoringControl.cs
@@ -6,17 +6,46 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  */
 
-using System;
+using Microsoft.Web.WebView2.Core;
 using Microsoft.Web.WebView2.Wpf;
+using SafeExamBrowser.Logging.Contracts;
 using SafeExamBrowser.UserInterface.Contracts.Proctoring;
 
 namespace SafeExamBrowser.Proctoring
 {
 	internal class ProctoringControl : WebView2, IProctoringControl
 	{
-		internal ProctoringControl()
+		private readonly ILogger logger;
+
+		internal ProctoringControl(ILogger logger)
 		{
-			Source = new Uri("https://www.microsoft.com");
+			this.logger = logger;
+			CoreWebView2InitializationCompleted += ProctoringControl_CoreWebView2InitializationCompleted;
+		}
+
+		private void CoreWebView2_PermissionRequested(object sender, CoreWebView2PermissionRequestedEventArgs e)
+		{
+			if (e.PermissionKind == CoreWebView2PermissionKind.Camera || e.PermissionKind == CoreWebView2PermissionKind.Microphone)
+			{
+				logger.Info($"Granted access to {e.PermissionKind}.");
+			}
+			else
+			{
+				logger.Info($"Denied access to {e.PermissionKind}.");
+			}
+		}
+
+		private void ProctoringControl_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e)
+		{
+			if (e.IsSuccess)
+			{
+				CoreWebView2.PermissionRequested += CoreWebView2_PermissionRequested;
+				logger.Info("Successfully initialized.");
+			}
+			else
+			{
+				logger.Error("Failed to initialize!", e.InitializationException);
+			}
 		}
 	}
 }
diff --git a/SafeExamBrowser.Proctoring/ProctoringController.cs b/SafeExamBrowser.Proctoring/ProctoringController.cs
index b98acf87..834e5c90 100644
--- a/SafeExamBrowser.Proctoring/ProctoringController.cs
+++ b/SafeExamBrowser.Proctoring/ProctoringController.cs
@@ -6,6 +6,10 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  */
 
+using System;
+using System.IO;
+using System.Reflection;
+using SafeExamBrowser.Logging.Contracts;
 using SafeExamBrowser.Proctoring.Contracts;
 using SafeExamBrowser.Settings.Proctoring;
 using SafeExamBrowser.UserInterface.Contracts;
@@ -15,31 +19,72 @@ namespace SafeExamBrowser.Proctoring
 {
 	public class ProctoringController : IProctoringController
 	{
+		private readonly IModuleLogger logger;
 		private readonly IUserInterfaceFactory uiFactory;
 
 		private IProctoringWindow window;
 
-		public ProctoringController(IUserInterfaceFactory uiFactory)
+		public ProctoringController(IModuleLogger logger, IUserInterfaceFactory uiFactory)
 		{
+			this.logger = logger;
 			this.uiFactory = uiFactory;
 		}
 
 		public void Initialize(ProctoringSettings settings)
 		{
-			var control = new ProctoringControl();
+			if (settings.JitsiMeet.Enabled || settings.Zoom.Enabled)
+			{
+				var control = new ProctoringControl(logger.CloneFor(nameof(ProctoringControl)));
 
-			window = uiFactory.CreateProctoringWindow(control);
-			window.Show();
+				control.EnsureCoreWebView2Async().ContinueWith(_ =>
+				{
+					control.Dispatcher.Invoke(() => control.NavigateToString(LoadContent(settings)));
+				});
 
-			// TODO
-			//var content = load Zoom page, replace //INDEX_JS//;
+				window = uiFactory.CreateProctoringWindow(control);
+				window.Show();
 
-			//control.NavigateToString(content);
+				logger.Info($"Initialized proctoring with {(settings.JitsiMeet.Enabled ? "Jitsi Meet" : "Zoom")}.");
+			}
+			else
+			{
+				logger.Warn("Failed to initialize remote proctoring because no provider is enabled in the active configuration.");
+			}
 		}
 
 		public void Terminate()
 		{
 			window?.Close();
 		}
+
+		private string LoadContent(ProctoringSettings settings)
+		{
+			var provider = settings.JitsiMeet.Enabled ? "JitsiMeet" : "Zoom";
+			var assembly = Assembly.GetAssembly(typeof(ProctoringController));
+			var path = $"{typeof(ProctoringController).Namespace}.{provider}.index.html";
+
+			using (var stream = assembly.GetManifestResourceStream(path))
+			using (var reader = new StreamReader(stream))
+			{
+				var html = reader.ReadToEnd();
+
+				if (settings.JitsiMeet.Enabled)
+				{
+					html = html.Replace("%%_DOMAIN_%%", settings.JitsiMeet.ServerUrl);
+					html = html.Replace("%%_ROOM_NAME_%%", settings.JitsiMeet.RoomName);
+					html = html.Replace("%%_SUBJECT_%%", settings.JitsiMeet.Subject);
+					html = html.Replace("%%_TOKEN_%%", settings.JitsiMeet.Token);
+				}
+				else if (settings.Zoom.Enabled)
+				{
+					html = html.Replace("%%_API_KEY_%%", settings.Zoom.ApiKey);
+					html = html.Replace("%%_API_SECRET_%%", settings.Zoom.ApiSecret);
+					html = html.Replace("123456789", Convert.ToString(settings.Zoom.MeetingNumber));
+					html = html.Replace("%%_USER_NAME_%%", settings.Zoom.UserName);
+				}
+
+				return html;
+			}
+		}
 	}
 }
diff --git a/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj b/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj
index 1c1bf007..c81eeb22 100644
--- a/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj
+++ b/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj
@@ -95,12 +95,10 @@
     <None Include="packages.config" />
   </ItemGroup>
   <ItemGroup>
-    <Content Include="Zoom\index.html" />
-    <Content Include="Zoom\index.js" />
-  </ItemGroup>
-  <ItemGroup>
-    <Folder Include="Jitsi\" />
+    <EmbeddedResource Include="JitsiMeet\index.html" />
+    <EmbeddedResource Include="Zoom\index.html" />
   </ItemGroup>
+  <ItemGroup />
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <Import Project="..\packages\Microsoft.Web.WebView2.1.0.705.50\build\Microsoft.Web.WebView2.targets" Condition="Exists('..\packages\Microsoft.Web.WebView2.1.0.705.50\build\Microsoft.Web.WebView2.targets')" />
   <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
diff --git a/SafeExamBrowser.Proctoring/Zoom/index.html b/SafeExamBrowser.Proctoring/Zoom/index.html
index e6151f97..a29ac0e8 100644
--- a/SafeExamBrowser.Proctoring/Zoom/index.html
+++ b/SafeExamBrowser.Proctoring/Zoom/index.html
@@ -14,7 +14,100 @@
         <script src="https://source.zoom.us/zoom-meeting-1.8.1.min.js"></script>
         <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9/crypto-js.min.js"></script>
         <script type="text/javascript">
-            //INDEX_JS//
+            const API_KEY = "%%_API_KEY_%%";
+            const API_SECRET = "%%_API_SECRET_%%";
+
+            console.log("Checking system requirements...");
+            console.log(JSON.stringify(ZoomMtg.checkSystemRequirements()));
+
+            console.log("Initializing Zoom...");
+            ZoomMtg.setZoomJSLib('https://source.zoom.us/1.8.1/lib', '/av');
+            ZoomMtg.preLoadWasm();
+            ZoomMtg.prepareJssdk();
+
+            const config = {
+                meetingNumber: 123456789,
+                leaveUrl: 'https://google.ch',
+                userName: '%%_USER_NAME_%%',
+                /* passWord: 'password', // if required */
+                role: 0 // 1 for host; 0 for attendee
+            };
+
+            const signature = ZoomMtg.generateSignature({
+                meetingNumber: config.meetingNumber,
+                apiKey: API_KEY,
+                apiSecret: API_SECRET,
+                role: config.role,
+                error: function (res) {
+                    console.error("FAILED TO GENERATE SIGNATURE: " + res)
+                },
+                success: function (res) {
+                    console.log("Successfully generated signature.");
+                    console.log(res.result);
+                },
+            });
+
+            console.log("Initializing meeting...");
+
+            // See documentation: https://zoom.github.io/sample-app-web/ZoomMtg.html#init
+            ZoomMtg.init({
+                debug: true, //optional
+                leaveUrl: config.leaveUrl, //required
+                // webEndpoint: 'PSO web domain', // PSO option
+                showMeetingHeader: true, //option
+                disableInvite: false, //optional
+                disableCallOut: false, //optional
+                disableRecord: false, //optional
+                disableJoinAudio: false, //optional
+                audioPanelAlwaysOpen: true, //optional
+                showPureSharingContent: false, //optional
+                isSupportAV: true, //optional,
+                isSupportChat: false, //optional,
+                isSupportQA: true, //optional,
+                isSupportCC: true, //optional,
+                screenShare: true, //optional,
+                rwcBackup: '', //optional,
+                videoDrag: true, //optional,
+                sharingMode: 'both', //optional,
+                videoHeader: true, //optional,
+                isLockBottom: true, // optional,
+                isSupportNonverbal: true, // optional,
+                isShowJoiningErrorDialog: true, // optional,
+                inviteUrlFormat: '', // optional
+                loginWindow: {  // optional,
+                    width: 400,
+                    height: 380
+                },
+                // meetingInfo: [ // optional
+                //   'topic',
+                //   'host',
+                //   'mn',
+                //   'pwd',
+                //   'telPwd',
+                //   'invite',
+                //   'participant',
+                //   'dc'
+                // ],
+                disableVoIP: false, // optional
+                disableReport: false, // optional
+                error: function (res) {
+                    console.warn("INIT ERROR")
+                    console.log(res)
+                },
+                success: function () {
+                    ZoomMtg.join({
+                        signature: signature,
+                        apiKey: API_KEY,
+                        meetingNumber: config.meetingNumber,
+                        userName: config.userName,
+                        /* passWord: meetConfig.passWord, */
+                        error(res) {
+                            console.warn("JOIN ERROR")
+                            console.log(res)
+                        }
+                    })
+                }
+            })
         </script>
     </body>
 </html>
\ No newline at end of file
diff --git a/SafeExamBrowser.Proctoring/Zoom/index.js b/SafeExamBrowser.Proctoring/Zoom/index.js
deleted file mode 100644
index 9adba733..00000000
--- a/SafeExamBrowser.Proctoring/Zoom/index.js
+++ /dev/null
@@ -1,95 +0,0 @@
-const API_KEY = "...";
-const API_SECRET = "...";
-
-console.log("Checking system requirements...");
-console.log(JSON.stringify(ZoomMtg.checkSystemRequirements()));
-
-console.log("Initializing Zoom...");
-ZoomMtg.setZoomJSLib('https://source.zoom.us/1.8.1/lib', '/av');
-ZoomMtg.preLoadWasm();
-ZoomMtg.prepareJssdk();
-
-const config = {
-    meetingNumber: 123456,
-    leaveUrl: 'https://google.ch',
-    userName: 'Firstname Lastname',
-    userEmail: 'firstname.lastname@yoursite.com',
-    /* passWord: 'password', // if required */
-    role: 0 // 1 for host; 0 for attendee
-};
-
-const signature = ZoomMtg.generateSignature({
-    meetingNumber: config.meetingNumber,
-    apiKey: API_KEY,
-    apiSecret: API_SECRET,
-    role: config.role,
-    error: function (res) {
-        console.error("FAILED TO GENERATE SIGNATURE: " + res)
-    },
-    success: function (res) {
-        console.log("Successfully generated signature.");
-        console.log(res.result);
-    },
-});
-
-console.log("Initializing meeting...");
-
-// See documentation: https://zoom.github.io/sample-app-web/ZoomMtg.html#init
-ZoomMtg.init({
-    debug: true, //optional
-    leaveUrl: config.leaveUrl, //required
-    // webEndpoint: 'PSO web domain', // PSO option
-    showMeetingHeader: true, //option
-    disableInvite: false, //optional
-    disableCallOut: false, //optional
-    disableRecord: false, //optional
-    disableJoinAudio: false, //optional
-    audioPanelAlwaysOpen: true, //optional
-    showPureSharingContent: false, //optional
-    isSupportAV: true, //optional,
-    isSupportChat: false, //optional,
-    isSupportQA: true, //optional,
-    isSupportCC: true, //optional,
-    screenShare: true, //optional,
-    rwcBackup: '', //optional,
-    videoDrag: true, //optional,
-    sharingMode: 'both', //optional,
-    videoHeader: true, //optional,
-    isLockBottom: true, // optional,
-    isSupportNonverbal: true, // optional,
-    isShowJoiningErrorDialog: true, // optional,
-    inviteUrlFormat: '', // optional
-    loginWindow: {  // optional,
-      width: 400,
-      height: 380
-    },
-    // meetingInfo: [ // optional
-    //   'topic',
-    //   'host',
-    //   'mn',
-    //   'pwd',
-    //   'telPwd',
-    //   'invite',
-    //   'participant',
-    //   'dc'
-    // ],
-    disableVoIP: false, // optional
-    disableReport: false, // optional
-    error: function(res) {
-        console.warn("INIT ERROR")
-        console.log(res)
-    },
-    success: function() {
-        ZoomMtg.join({
-            signature: signature,
-            apiKey: API_KEY,
-            meetingNumber: config.meetingNumber,
-            userName: config.userName,
-            /* passWord: meetConfig.passWord, */
-            error(res) {
-                console.warn("JOIN ERROR")
-                console.log(res)
-            }
-        })
-    }
-})
diff --git a/SafeExamBrowser.Settings/Proctoring/JitsiMeetSettings.cs b/SafeExamBrowser.Settings/Proctoring/JitsiMeetSettings.cs
new file mode 100644
index 00000000..ff8bb029
--- /dev/null
+++ b/SafeExamBrowser.Settings/Proctoring/JitsiMeetSettings.cs
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2021 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 meeting provider Jitsi Meet.
+	/// </summary>
+	[Serializable]
+	public class JitsiMeetSettings
+	{
+		/// <summary>
+		/// Determines whether proctoring with Jitsi Meet is enabled.
+		/// </summary>
+		public bool Enabled { get; set; }
+
+		/// <summary>
+		/// The name of the meeting room.
+		/// </summary>
+		public string RoomName { get; set; }
+
+		/// <summary>
+		/// The URL of the Jitsi Meet server.
+		/// </summary>
+		public string ServerUrl { get; set; }
+
+		/// <summary>
+		/// The subject of the meeting.
+		/// </summary>
+		public string Subject { get; set; }
+
+		/// <summary>
+		/// The authentication token for the meeting.
+		/// </summary>
+		public string Token { get; set; }
+	}
+}
diff --git a/SafeExamBrowser.Settings/Proctoring/ProctoringSettings.cs b/SafeExamBrowser.Settings/Proctoring/ProctoringSettings.cs
index 306c68e8..cb14ccb8 100644
--- a/SafeExamBrowser.Settings/Proctoring/ProctoringSettings.cs
+++ b/SafeExamBrowser.Settings/Proctoring/ProctoringSettings.cs
@@ -20,5 +20,21 @@ namespace SafeExamBrowser.Settings.Proctoring
 		/// Determines whether the entire remote proctoring feature is enabled.
 		/// </summary>
 		public bool Enabled { get; set; }
+
+		/// <summary>
+		/// All settings for remote proctoring with Jitsi Meet.
+		/// </summary>
+		public JitsiMeetSettings JitsiMeet { get; set; }
+
+		/// <summary>
+		/// All settings for remote proctoring with Zoom.
+		/// </summary>
+		public ZoomSettings Zoom { get; set; }
+
+		public ProctoringSettings()
+		{
+			JitsiMeet = new JitsiMeetSettings();
+			Zoom = new ZoomSettings();
+		}
 	}
 }
diff --git a/SafeExamBrowser.Settings/Proctoring/ZoomSettings.cs b/SafeExamBrowser.Settings/Proctoring/ZoomSettings.cs
new file mode 100644
index 00000000..85739958
--- /dev/null
+++ b/SafeExamBrowser.Settings/Proctoring/ZoomSettings.cs
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2021 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 meeting provider Zoom.
+	/// </summary>
+	[Serializable]
+	public class ZoomSettings
+	{
+		/// <summary>
+		/// The API key to be used for authentication.
+		/// </summary>
+		public string ApiKey { get; set; }
+
+		/// <summary>
+		/// The API secret to be used for authentication.
+		/// </summary>
+		public string ApiSecret { get; set; }
+
+		/// <summary>
+		/// Determines whether proctoring with Zoom is enabled.
+		/// </summary>
+		public bool Enabled { get; set; }
+
+		/// <summary>
+		/// The number of the meeting.
+		/// </summary>
+		public int MeetingNumber { get; set; }
+
+		/// <summary>
+		/// The user name to be used for the meeting.
+		/// </summary>
+		public string UserName { get; set; }
+	}
+}
diff --git a/SafeExamBrowser.Settings/SafeExamBrowser.Settings.csproj b/SafeExamBrowser.Settings/SafeExamBrowser.Settings.csproj
index 5f85298c..6780fbb9 100644
--- a/SafeExamBrowser.Settings/SafeExamBrowser.Settings.csproj
+++ b/SafeExamBrowser.Settings/SafeExamBrowser.Settings.csproj
@@ -71,7 +71,9 @@
     <Compile Include="Browser\Proxy\ProxyProtocol.cs" />
     <Compile Include="Browser\Proxy\ProxyConfiguration.cs" />
     <Compile Include="ConfigurationMode.cs" />
+    <Compile Include="Proctoring\JitsiMeetSettings.cs" />
     <Compile Include="Proctoring\ProctoringSettings.cs" />
+    <Compile Include="Proctoring\ZoomSettings.cs" />
     <Compile Include="SessionMode.cs" />
     <Compile Include="Security\KioskMode.cs" />
     <Compile Include="Logging\LogLevel.cs" />