Index: openacs-4/packages/proctoring-support/lib/proctored-page.adp =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/proctoring-support/lib/proctored-page.adp,v diff -u -r1.1.2.10 -r1.1.2.11 --- openacs-4/packages/proctoring-support/lib/proctored-page.adp 6 Oct 2020 17:09:13 -0000 1.1.2.10 +++ openacs-4/packages/proctoring-support/lib/proctored-page.adp 22 Oct 2020 14:22:20 -0000 1.1.2.11 @@ -29,21 +29,27 @@
@msg.proctoring_accept@
-
-

#proctoring-support.grant_access_to_microphone_title#

-
#proctoring-support.grant_access_to_microphone_msg#
- -
-
-

#proctoring-support.grant_access_to_camera_title#

-
#proctoring-support.grant_access_to_camera_msg#
- -
-
-

#proctoring-support.grant_access_to_desktop_title#

-
#proctoring-support.grant_access_to_desktop_msg#
- -
+ +
+

#proctoring-support.grant_access_to_microphone_title#

+
#proctoring-support.grant_access_to_microphone_msg#
+ +
+
+ +
+

#proctoring-support.grant_access_to_camera_title#

+
#proctoring-support.grant_access_to_camera_msg#
+ +
+
+ +
+

#proctoring-support.grant_access_to_desktop_title#

+
#proctoring-support.grant_access_to_desktop_msg#
+ +
+
@@ -64,10 +70,16 @@ + + + + + + + + + - - -
@@ -90,6 +102,8 @@ var examinationStatementURL = "@examination_statement_url;literal@"; var hasPreview = @preview_p;literal@; var hasProctoring = @proctoring_p;literal@; + var hasCamera = @camera_p;literal@; + var hasDesktop = @desktop_p;literal@; var hasAudio = @audio_p;literal@; var minMsInterval = @min_ms_interval;literal@; var maxMsInterval = @max_ms_interval;literal@; Index: openacs-4/packages/proctoring-support/lib/proctored-page.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/proctoring-support/lib/proctored-page.tcl,v diff -u -r1.1.2.5 -r1.1.2.6 --- openacs-4/packages/proctoring-support/lib/proctored-page.tcl 23 Sep 2020 17:23:29 -0000 1.1.2.5 +++ openacs-4/packages/proctoring-support/lib/proctored-page.tcl 22 Oct 2020 14:22:20 -0000 1.1.2.6 @@ -37,6 +37,10 @@ be displayed to users during proctored session @param proctoring_p Do the actual proctoring. Can be disabled to display just the exmaination statement + @param camera_p proctor the camera. If false, camera will not be + recorded. + @param desktop_p proctor the desktop screen. If false, desktop + screen will not be recorded. @param examination_statement_p Display the examination statement @param examination_statement_url URL we are calling in order to store acceptance of the examination statement. It will @@ -70,6 +74,8 @@ {max_audio_duration:naturalnum 60} {preview_p:boolean false} {proctoring_p:boolean true} + {camera_p:boolean true} + {desktop_p:boolean true} {check_active_p:boolean true} {examination_statement_p:boolean true} {examination_statement_url:localurl "/proctoring/examination-statement-accept"} @@ -113,5 +119,9 @@ set mobile_p [ad_conn mobile_p] set check_active_p [expr {$check_active_p ? true : false}] set preview_p [expr {$preview_p ? true : false}] -set proctoring_p [expr {$proctoring_p ? true : false}] +set proctoring_p [expr {$proctoring_p && + ($camera_p || $audio_p || $desktop_p) ? true : false}] set upload_p [expr {$upload_p ? true : false}] +set audio_p [expr {$audio_p ? true : false}] +set camera_p [expr {$camera_p ? true : false}] +set desktop_p [expr {$desktop_p ? true : false}] Index: openacs-4/packages/proctoring-support/www/resources/proctored-page.js =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/proctoring-support/www/resources/proctored-page.js,v diff -u -r1.1.2.15 -r1.1.2.16 --- openacs-4/packages/proctoring-support/www/resources/proctored-page.js 8 Oct 2020 14:04:10 -0000 1.1.2.15 +++ openacs-4/packages/proctoring-support/www/resources/proctored-page.js 22 Oct 2020 14:22:20 -0000 1.1.2.16 @@ -71,18 +71,24 @@ var e = document.querySelector("#preview-placeholder"); style = !hasPreview ? "position:absolute;top:0;left:0;" : ""; e.setAttribute("style", style); - var canvas = document.createElement("canvas"); - style = hasPreview ? "height: 30px; width: 40px" : "height: 1px; width: 1px"; - canvas.setAttribute("style", style); - canvas.setAttribute("id", "audio-preview"); - e.appendChild(canvas); - new AudioWave(proctoring.streams[0], "#audio-preview"); - for (var i = 0; i < proctoring.videos.length; i++) { - var video = proctoring.videos[i]; - var width = hasPreview ? 30 : 1; - video.setAttribute("height", width); - e.appendChild(video); + for (stream of proctoring.streams) { + if (stream && stream.getAudioTracks().length > 0) { + var canvas = document.createElement("canvas"); + style = hasPreview ? "height: 30px; width: 40px" : "height: 1px; width: 1px"; + canvas.setAttribute("style", style); + canvas.setAttribute("id", "audio-preview"); + e.appendChild(canvas); + new AudioWave(stream, "#audio-preview"); + break; + } } + for (video of proctoring.videos) { + if (video) { + var width = hasPreview ? 30 : 1; + video.setAttribute("height", width); + e.appendChild(video); + } + } } var uploadHandle = null; @@ -222,100 +228,105 @@ setError(errmsg); } }); - handlers.push(function () { - valid = false; - clearError(); - var constraints = { - audio: cameraConstraints.audio - }; - navigator.mediaDevices.getUserMedia(constraints).then(stream => { - if (streamMuted(stream)) { - throw yourMicrophoneIsMutedMessage; - } else { + if (hasAudio) { + handlers.push(function () { + valid = false; + clearError(); + var constraints = { + audio: cameraConstraints.audio + }; + navigator.mediaDevices.getUserMedia(constraints).then(stream => { + if (streamMuted(stream)) { + throw yourMicrophoneIsMutedMessage; + } else { + new AudioWave(stream, "#audio"); + valid = true; + streams[0] = stream; + } + }).catch(err => { + if (err.name == "NotAllowedError") { + err = microphonePermissionDeniedMessage; + } else if (err.name == "NotFoundError") { + err = microphoneNotFoundMessage; + } else if (err.name == "NotReadableError") { + err = microphoneNotReadableMessage; + } + setError(err); + }); + }); + } + if (hasCamera) { + handlers.push(function () { + valid = false; + clearError(); + var constraints = { + video: cameraConstraints.video + }; + navigator.mediaDevices.getUserMedia(constraints).then(stream => { camvideo.srcObject = stream; - new AudioWave(stream, "#audio"); + camvideo.style.display = "block"; + streams[1] = stream; + camvideo.addEventListener("play", function() { + var canvas = document.createElement("canvas"); + canvas.width = camvideo.videoWidth; + canvas.height = camvideo.videoHeight; + canvas.getContext("2d").drawImage(camvideo, 0, 0, camvideo.videoWidth, camvideo.videoHeight); + canvas.toBlob(function(blob) { + if (blob == null || + blob.size <= blackPictureSizeThreshold) { + var errmsg = blackPictureCameraMessage; + setError(errmsg); + } + }, "image/jpeg"); + }); valid = true; - streams[0] = stream; - } - }).catch(err => { - if (err.name == "NotAllowedError") { - err = microphonePermissionDeniedMessage; - } else if (err.name == "NotFoundError") { - err = microphoneNotFoundMessage; - } else if (err.name == "NotReadableError") { - err = microphoneNotReadableMessage; - } - setError(err); - }); - }); - handlers.push(function () { - valid = false; - clearError(); - var constraints = { - video: cameraConstraints.video - }; - navigator.mediaDevices.getUserMedia(constraints).then(stream => { - camvideo.srcObject = stream; - camvideo.style.display = "block"; - streams[1] = stream; - camvideo.addEventListener("play", function() { - var canvas = document.createElement("canvas"); - canvas.width = camvideo.videoWidth; - canvas.height = camvideo.videoHeight; - canvas.getContext("2d").drawImage(camvideo, 0, 0, camvideo.videoWidth, camvideo.videoHeight); - canvas.toBlob(function(blob) { - if (blob == null || - blob.size <= blackPictureSizeThreshold) { - var errmsg = blackPictureCameraMessage; - setError(errmsg); - } - }, "image/jpeg"); + }).catch(err => { + if (err.name == "NotAllowedError") { + err = cameraPermissionDeniedMessage; + } else if (err.name == "NotFoundError") { + err = cameraNotFoundMessage; + } else if (err.name == "NotReadableError") { + err = cameraNotReadableMessage; + } + setError(err); }); - valid = true; - }).catch(err => { - if (err.name == "NotAllowedError") { - err = cameraPermissionDeniedMessage; - } else if (err.name == "NotFoundError") { - err = cameraNotFoundMessage; - } else if (err.name == "NotReadableError") { - err = cameraNotReadableMessage; - } - setError(err); }); - }); - handlers.push(function () { - valid = false; - clearError(); - var constraints = { - video: desktopConstraints.video - }; - navigator.mediaDevices.getDisplayMedia(constraints).then(stream=> { - var requestedStream = constraints.video.displaySurface; - var selectedStream = stream.getVideoTracks()[0].getSettings().displaySurface; - // If user requested for a specific displaysurface - // and browser supports it, also check that the - // one selected is right. - if (requestedStream == undefined || - (selectedStream != undefined && - requestedStream == selectedStream)) { - deskvideo.srcObject = stream; - deskvideo.style.display = "block"; - valid = true; - streams[2] = stream; - } else { - if (selectedStream != undefined) { - throw wrongDisplaySurfaceSelectedMessage; + } + if (hasDesktop) { + handlers.push(function () { + valid = false; + clearError(); + var constraints = { + video: desktopConstraints.video + }; + navigator.mediaDevices.getDisplayMedia(constraints).then(stream=> { + var requestedStream = constraints.video.displaySurface; + var selectedStream = stream.getVideoTracks()[0].getSettings().displaySurface; + // If user requested for a specific displaysurface + // and browser supports it, also check that the + // one selected is right. + if (requestedStream == undefined || + (selectedStream != undefined && + requestedStream == selectedStream)) { + deskvideo.srcObject = stream; + deskvideo.style.display = "block"; + valid = true; + streams[2] = stream; } else { - throw displaySurfaceNotSupportedMessage; + if (selectedStream != undefined) { + throw wrongDisplaySurfaceSelectedMessage; + } else { + throw displaySurfaceNotSupportedMessage; + } } - } - }).catch(err => { - if (err.name == "NotAllowedError") { - err = desktopPermissionDeniedMessage; - } - setError(err); + }).catch(err => { + if (err.name == "NotAllowedError") { + err = desktopPermissionDeniedMessage; + } + setError(err); + }); }); - }); + } } if (hasExaminationStatement) { handlers.push(function () { @@ -332,6 +343,7 @@ function showTab(n) { // This function will display the specified tab of the form... var x = document.getElementsByClassName("tab"); + if (x.length == 0) return; x[n].style.display = "block"; //... and fix the Previous/Next buttons: if (n == 0) { @@ -442,8 +454,48 @@ document.querySelector("#wizard").style.display = "none"; document.querySelector("#proctoring").style.display = "block"; - var cameraStream = embedAudioTrackFromStream(streams[0], streams[1]); + var mediaConf = {}; + var cameraStream; + if (hasAudio && hasCamera) { + cameraStream = embedAudioTrackFromStream(streams[0], streams[1]); + } else if (hasAudio) { + cameraStream = streams[0]; + } else if (hasCamera) { + cameraStream = streams[1]; + } + if (hasAudio || hasCamera) { + mediaConf.camera = { + required: true, + grayscale: true, + width: 320, + height: 240, + imageHandlers: { + jpeg: { + blob: function(blob) { + scheduleUpload("camera", "image", blob); + } + } + }, + audioHandlers: audioHandlers, + stream: cameraStream + }; + } var desktopStream = streams[2]; + if (hasDesktop) { + mediaConf.desktop = { + required: true, + grayscale: false, + imageHandlers: { + jpeg: { + blob: function(blob) { + scheduleUpload("desktop", "image", blob); + } + } + }, + stream: desktopStream + }; + } + var conf = { minMsInterval: minMsInterval, maxMsInterval: maxMsInterval, @@ -462,35 +514,7 @@ createIframe(); createPreview(); }, - mediaConf: { - camera: { - required: true, - grayscale: true, - width: 320, - height: 240, - imageHandlers: { - jpeg: { - blob: function(blob) { - scheduleUpload("camera", "image", blob); - } - } - }, - audioHandlers: audioHandlers, - stream: cameraStream - }, - desktop: { - required: true, - grayscale: false, - imageHandlers: { - jpeg: { - blob: function(blob) { - scheduleUpload("desktop", "image", blob); - } - } - }, - stream: desktopStream - } - } + mediaConf: mediaConf }; if (hasProctoring) { Index: openacs-4/packages/proctoring-support/www/resources/proctoring.js =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/proctoring-support/www/resources/proctoring.js,v diff -u -r1.1.2.6 -r1.1.2.7 --- openacs-4/packages/proctoring-support/www/resources/proctoring.js 7 Oct 2020 10:12:31 -0000 1.1.2.6 +++ openacs-4/packages/proctoring-support/www/resources/proctoring.js 22 Oct 2020 14:22:20 -0000 1.1.2.7 @@ -398,7 +398,9 @@ this.maxAudioDuration).start(); } this.streams[i] = stream; - this.videos[i] = this.createVideo(stream); + if (stream.getVideoTracks().length > 0) { + this.videos[i] = this.createVideo(stream); + } this.numActiveStreams++; this.numCheckedStreams++; } @@ -529,7 +531,7 @@ throw "stream is not active"; } else if (this.streamMuted(stream)) { throw "stream is muted"; - } else if (this.ready && video.paused) { + } else if (this.ready && video && video.paused) { throw "video acquisition appears to have stopped"; } } catch (e) { @@ -794,9 +796,9 @@ } this.ready = true; } - // For every configured stream, take a picture + // For every configured video stream, take a picture for (var i = 0; i < this.streams.length; i++) { - if (this.streams[i] != null) { + if (this.videos[i]) { this.takeShot(this.streams[i], this.mediaConf[this.streamNames[i]].grayscale); } }