+ }
+ >
)
}
diff --git a/src/hooks/ProctorService.ts b/src/hooks/ProctorService.ts
new file mode 100644
index 0000000..2957a2f
--- /dev/null
+++ b/src/hooks/ProctorService.ts
@@ -0,0 +1,102 @@
+import {
+ FaceDetector,
+ FaceLandmarker,
+ FilesetResolver,
+ ObjectDetector,
+} from '@mediapipe/tasks-vision'
+
+/**
+ * The `ProctorService` class in TypeScript initializes and manages instances of face detection, face
+ * landmarking, and object detection classes from the `@mediapipe/tasks-vision` library.
+ */
+class ProctorService {
+ /**
+ * The `faceDetector` property in the `ProctorService` class is used to store an instance of the
+ * FaceDetector class from the `@mediapipe/tasks-vision` library. This instance is created during the
+ * initialization of the `ProctorService` class and is used to detect faces in images or video
+ * streams. The `faceDetector` property allows the `ProctorService` class to perform face detection
+ * tasks using the functionalities provided by the FaceDetector class.
+ */
+ faceDetector?: FaceDetector
+
+ /**
+ * The `faceLandmarker?: FaceLandmarker` line in the `ProctorService` class is declaring a property
+ * named `faceLandmarker` that can hold an instance of the `FaceLandmarker` class from the
+ * `@mediapipe/tasks-vision` library.
+ */
+ faceLandmarker?: FaceLandmarker
+
+ /**
+ * The line `objectDetector?: ObjectDetector` in the `ProctorService` class is declaring a property
+ * named `objectDetector` that can hold an instance of the `ObjectDetector` class from the
+ * `@mediapipe/tasks-vision` library. This property is used to
+ * store an instance of the `ObjectDetector` class, which is responsible for detecting objects in
+ * images or video streams using the functionalities provided by the `ObjectDetector` class.
+ */
+ objectDetector?: ObjectDetector
+
+ private eyeTracker: any
+ private vision: any
+
+ /**
+ * The `ProctorService` class in TypeScript initializes and manages instances of face detection, face
+ * landmarking, and object detection classes from the `@mediapipe/tasks-vision` library.
+ */
+ static instance: ProctorService
+
+ violations = {
+ facesDetected: 0,
+ }
+
+ constructor() {
+ this.initVision()
+ }
+
+ private async initVision() {
+ this.vision = await FilesetResolver.forVisionTasks(
+ 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm',
+ )
+
+ this.faceDetector = await FaceDetector.createFromOptions(this.vision, {
+ baseOptions: {
+ modelAssetPath:
+ 'https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/latest/blaze_face_short_range.tflite',
+ delegate: 'GPU',
+ },
+ runningMode: 'VIDEO',
+ })
+
+ this.faceLandmarker = await FaceLandmarker.createFromOptions(this.vision, {
+ baseOptions: {
+ modelAssetPath:
+ 'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task',
+ delegate: 'CPU',
+ },
+ runningMode: 'VIDEO',
+ numFaces: 1,
+ })
+
+ this.objectDetector = await ObjectDetector.createFromOptions(this.vision, {
+ baseOptions: {
+ modelAssetPath:
+ 'https://storage.googleapis.com/mediapipe-tasks/object_detector/efficientdet_lite0_uint8.tflite',
+ delegate: 'CPU',
+ },
+ runningMode: 'VIDEO',
+ scoreThreshold: 0.2,
+ })
+ }
+
+ /**
+ * Returns a singleton instance of the ProctorService class.
+ * @returns {ProctorService} Proctor - The singleton instance of ProctorService.
+ */
+ static getInstance() {
+ if (!ProctorService.instance) {
+ ProctorService.instance = new ProctorService()
+ }
+ return ProctorService.instance
+ }
+}
+
+export default ProctorService.getInstance
diff --git a/src/hooks/useCam.ts b/src/hooks/useCam.ts
new file mode 100644
index 0000000..94f8c24
--- /dev/null
+++ b/src/hooks/useCam.ts
@@ -0,0 +1,124 @@
+import { RefObject, useEffect, useMemo, useRef, useState } from 'react'
+import ProctorService from './ProctorService'
+
+/**
+ * useCam is a hook that allows you to detect faces and objects in a video stream.
+ * It also tracks the user's eyes and detects if the user is looking at the camera.
+ * The hook returns an object with the following properties:
+ * - videoRef: a reference to the video element that the hook is controlling.
+ * - violationStatus: an object with the following properties:
+ * - facesDetected: the number of faces detected in the video stream.
+ * - objectDetected: an array of strings representing the objects detected in the video stream.
+ * - startWebcam: a function that starts the webcam and begins the face and object detection.
+ *
+ * @param {{ disabled?: boolean }} options - an object with the following properties:
+ * - disabled: a boolean indicating whether the webcam should be disabled.
+ * If set to true, the hook will not request access to the webcam and will not detect faces or objects.
+ * Defaults to false.
+ *
+ * @returns {{ videoRef: RefObject, violationStatus: { facesDetected: number, objectDetected: string[] }, startWebcam: () => void }}
+ */
+export const useCam = ({ disabled }: { disabled?: boolean }) => {
+ const videoRef = useRef(null)
+ const proctorRef = useRef(ProctorService)
+
+ const [violationStatus, setViolationStatus] = useState<{
+ facesDetected: number
+ objectDetected: string[]
+ }>({
+ facesDetected: 0,
+ objectDetected: [],
+ })
+
+ const proctor = useMemo(() => proctorRef.current(), [])
+
+ useEffect(() => {
+ if (!disabled) {
+ startWebcam()
+ }
+
+ return () => {
+ if (videoRef.current && videoRef.current.srcObject) {
+ ;(videoRef.current.srcObject as MediaStream).getTracks().forEach((track) => track.stop())
+ }
+ }
+ }, [])
+
+ /**
+ * Detects faces in a video stream.
+ * @param videoRef a reference to the video element that the hook is controlling.
+ * @returns an array of face detections, or undefined if the video stream is not ready.
+ */
+ const detectFaces = (videoRef: RefObject) => {
+ if (videoRef.current && videoRef.current.readyState >= 2 && proctor.faceDetector) {
+ const detections = proctor.faceDetector.detectForVideo(videoRef.current, performance.now())
+ setViolationStatus((prev) => ({ ...prev, facesDetected: detections.detections.length }))
+ requestAnimationFrame(detectFaces.bind(this, videoRef))
+ return detections.detections
+ }
+ window.requestAnimationFrame(detectFaces.bind(this, videoRef))
+ }
+
+ /**
+ * Tracks eye movements in a video stream using face landmarks.
+ * Continuously requests animation frames to keep detecting eye movements.
+ * If face landmarks are detected, it processes the detections for eye tracking.
+ * @param videoRef A reference to the video element being used for eye tracking.
+ */
+ const eyesTracker = (videoRef: RefObject) => {
+ if (videoRef.current && videoRef.current.readyState >= 2 && proctor.faceLandmarker) {
+ const detections = proctor.faceLandmarker.detectForVideo(videoRef.current, performance.now())
+
+ // console.log(detections.faceBlendshapes)
+ // setViolationStatus((prev) => ({ ...prev, objectDetected: detections.detections.length }))
+ requestAnimationFrame(eyesTracker.bind(this, videoRef))
+ // return detections.detections
+ }
+ requestAnimationFrame(eyesTracker.bind(this, videoRef))
+ }
+
+ /**
+ * Detects objects in a video stream using the object detector.
+ * Continuously requests animation frames to keep detecting objects.
+ * If object detections are detected, it processes the detections and updates the violation status.
+ * @param videoRef A reference to the video element being used for object detection.
+ * @returns an array of object detections, or undefined if the video stream is not ready.
+ */
+ const objectDetection = (videoRef: RefObject) => {
+ if (videoRef.current && videoRef.current.readyState >= 2 && proctor.objectDetector) {
+ const detections = proctor.objectDetector.detectForVideo(videoRef.current, performance.now())
+ // setViolationStatus((prev) => ({ ...prev, objectDetected: detections.detections.length }))
+ setViolationStatus((prev) => {
+ const categories = detections.detections.map(
+ (detection) => detection.categories[0].categoryName,
+ )
+ return { ...prev, objectDetected: categories }
+ })
+ requestAnimationFrame(objectDetection.bind(this, videoRef))
+ return detections.detections
+ }
+ requestAnimationFrame(objectDetection.bind(this, videoRef))
+ }
+
+ /**
+ * Requests access to the user's webcam and starts the face detection, eye tracking and object detection.
+ * @throws {Error} If there is an error accessing the user's webcam.
+ */
+ const startWebcam = async () => {
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true })
+ if (videoRef.current) videoRef.current.srcObject = stream
+ detectFaces(videoRef)
+ // eyesTracker(videoRef)
+ objectDetection(videoRef)
+ } catch (error) {
+ console.error('Error accessing webcam:', error)
+ }
+ }
+
+ return {
+ // startWebcam,
+ violationStatus,
+ videoRef,
+ }
+}
diff --git a/src/hooks/useProctoring.ts b/src/hooks/useProctoring.ts
index 1348415..20d588c 100644
--- a/src/hooks/useProctoring.ts
+++ b/src/hooks/useProctoring.ts
@@ -9,6 +9,9 @@ import { useTabFocusDetection } from './useTabFocusDetection'
import { useCopyDisable } from './useCopyDisable'
import { useDisableContextMenu } from './useDisableContextMenu'
import { useSelectionDisable } from './useSelectionDisable'
+import ProctorService from './ProctorService'
+import { RefObject } from 'react'
+import { useCam } from './useCam'
type Props = {
preventContextMenu?: boolean
@@ -16,6 +19,7 @@ type Props = {
preventCopy?: boolean
forceFullScreen?: boolean
preventTabSwitch?: boolean
+ monitorCam?: boolean
}
export type ProctoringData = {
@@ -23,13 +27,68 @@ export type ProctoringData = {
tabFocus: { status: boolean }
}
+export type ProctorObject = {
+ fullScreen: {
+ status: FullScreenStatus
+ trigger: () => void
+ }
+ tabFocus: {
+ status: boolean
+ }
+ camDetection: {
+ violationStatus: {
+ facesDetected: number
+ objectDetected: string[]
+ }
+ videoRef: RefObject
+ }
+}
+
+/**
+ * This hook provides a way to detect and prevent various kinds of user behaviors
+ * that are deemed malicious or unwanted. It is meant to be used in an exam
+ * context and will throw an error if the user tries to switch tabs, open the
+ * context menu, select text, copy text, or does not have camera permissions.
+ *
+ * @param {Object} props
+ * @param {boolean} [props.preventContextMenu=false] - Whether to disable the
+ * context menu (right click menu)
+ * @param {boolean} [props.preventUserSelection=false] - Whether to disable text
+ * selection
+ * @param {boolean} [props.preventCopy=false] - Whether to prevent text copying
+ * @param {boolean} [props.forceFullScreen=false] - Whether to force the browser
+ * into full screen mode
+ * @param {boolean} [props.preventTabSwitch=false] - Whether to prevent the user
+ * from switching tabs
+ * @param {boolean} [props.monitorCam=false] - Whether to monitor the user's
+ * camera for faces and objects
+ *
+ * @returns {ProctorObject} An object containing the following properties:
+ * - fullScreen: An object with two properties: `status` and `trigger`. The
+ * `status` property is a string indicating whether the browser is in full
+ * screen mode or not, and the `trigger` property is a function that can be
+ * called to trigger full screen mode.
+ * - tabFocus: An object with one property: `status`. The `status` property is
+ * a boolean indicating whether the user is currently focused on the current
+ * tab or not.
+ * - camDetection: An object with two properties: `violationStatus` and
+ * `videoRef`. The `violationStatus` property is an object containing two
+ * properties: `facesDetected` and `objectDetected`. The `facesDetected`
+ * property is a number indicating how many faces were detected in the
+ * camera, and the `objectDetected` property is an array of strings
+ * indicating which objects were detected in the camera. The `videoRef`
+ * property is a reference to the video element that is used for camera
+ * monitoring.
+ */
+
export function useProctoring({
preventTabSwitch = false,
forceFullScreen = false,
preventContextMenu = false,
preventUserSelection = false,
preventCopy = false,
-}: Props) {
+ monitorCam = false,
+}: Props): ProctorObject {
useDisableContextMenu({ disabled: preventContextMenu === false })
useCopyDisable({ disabled: preventCopy === false })
@@ -49,9 +108,11 @@ export function useProctoring({
const { fullScreenStatus } = useFullScreenDetection({
disabled: forceFullScreen === false,
})
+ const { videoRef, violationStatus } = useCam({ disabled: monitorCam === false })
return {
fullScreen: { status: fullScreenStatus, trigger: triggerFullscreen },
tabFocus: { status: tabFocusStatus },
- } as const
+ camDetection: { violationStatus, videoRef },
+ }
}
diff --git a/src/index.ts b/src/index.ts
index b3fad48..7cb5acf 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -3,7 +3,7 @@
// import { useDisableContextMenu } from './hooks/useDisableContextMenu'
// import { useFullScreenDetection } from './hooks/useFullScreenDetection'
// import { useIntersectionObserver } from './hooks/useIntersectionObserver'
-import { useProctoring } from './hooks/useProctoring'
+import { useProctoring, ProctorObject } from './hooks/useProctoring'
// import { useSelectionDisable } from './hooks/useSelectionDisable'
// import { useTabFocusDetection } from './hooks/useTabFocusDetection'
@@ -14,6 +14,7 @@ export {
// useFullScreenDetection,
// useIntersectionObserver,
useProctoring,
+ type ProctorObject,
// useSelectionDisable,
// useTabFocusDetection,
}
diff --git a/yarn.lock b/yarn.lock
index 775c7f9..f277f2a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -393,6 +393,11 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
+"@mediapipe/tasks-vision@^0.10.18":
+ version "0.10.18"
+ resolved "https://registry.yarnpkg.com/@mediapipe/tasks-vision/-/tasks-vision-0.10.18.tgz#9ea0f0bf7506378c55ee661fa70aa0910f21f9b5"
+ integrity sha512-NRIlyqhGUz1Jdgcs6YybwPRhLK6dgeGAqAMXepIczEQ7FmA/0ouFtgMO1g9SPf/HaDSO8pNVdP54dAb9s9wj/Q==
+
"@microsoft/api-extractor-model@7.26.4":
version "7.26.4"
resolved "https://registry.yarnpkg.com/@microsoft/api-extractor-model/-/api-extractor-model-7.26.4.tgz#77f2c17140249b846a61eea41e565289cc77181f"