@peepl/proctoring-browser
v0.1.0
Published
Proctoring SDK for client pages embedding the PEEPL online test via iframe
Downloads
306
Readme
@peepl/proctoring-browser
Embed the PEEPL Online Test with an iframe while proctoring runs on your page. Initialize the SDK once; the assessment controls when camera, screen sharing, and profile-photo capture are needed. Candidate login stays inside the iframe.
Install
bun add @peepl/proctoring-browserFor a script-tag integration, use the browser bundle shown below. To host it yourself, see self-hosting. Registry and CDN installation require the package to be published there.
Basic usage
<iframe id="online-test" src="https://test.peepl.tech/" allow="camera; microphone"></iframe>
<script src="https://unpkg.com/@peepl/proctoring-browser/dist/index.iife.js"></script>
<script>
const proctor = NucleusProctor.init({ iframe: '#online-test' });
window.addEventListener('pagehide', () => proctor.destroy());
</script>Use your PEEPL assessment URL, HTTPS, desktop Chrome/Edge, and a parent-page
origin registered with PEEPL. Keep the iframe visible during the assessment.
allow="camera; microphone" permits interview and presentation media inside
the iframe.
Use your own AI backend
PEEPL uses internal camera analysis by default. To use your own AI, PEEPL must
enable external analysis for your client and issue an API key for your backend.
Your backend runs inference and submits results using
@peepl/proctoring-server.
Browser face detection, profile photos, periodic captures, screen evidence, and clipboard/window monitoring remain separate from final AI analysis. PEEPL still handles evidence storage, incident rules, and reporting.
Client page
import { init, createCameraAnalyzer } from '@peepl/proctoring-browser'
const proctor = init({
iframe: '#online-test',
analyzeCamera: createCameraAnalyzer('/api/proctoring/analyze'),
})
proctor.on('analysis-error', ({ evidenceId, code }) => {
console.warn(evidenceId, code)
})
window.addEventListener('pagehide', () => proctor.destroy())For script-tag integration, use
NucleusProctor.createCameraAnalyzer('/api/proctoring/analyze').
createCameraAnalyzer sends JSON by same-origin POST with the page's session
cookies. It requires HTTPS except for local loopback development and rejects
cross-origin URLs, URL credentials, and redirects. Supply additional headers
with its headers option when your endpoint requires CSRF protection.
You can also supply your own async analyzeCamera callback for a different
authentication scheme or response format. It receives the request below plus
an abort signal and must return { status: 'submitted' } after backend submission.
What your endpoint receives
{
"image": "data:image/jpeg;base64,...",
"evidenceId": "cam_0123456789abcdef",
"sessionId": "emb_0123456789abcdef0123456789abcdef",
"capturedAt": "2026-09-30T03:00:00.000Z"
}Authenticate and authorize the candidate, enforce CSRF protection and request
limits, and run inference on image on your server. Keep AI credentials and
the PEEPL API key out of the browser. Do not trust browser-supplied detections.
After submitting to PEEPL, return this JSON response:
{ "status": "submitted" }See the server guide for the model result contract and submission example.
Runtime behavior
- Only camera frames selected for incident checking invoke
analyzeCamera, not every video frame or ordinary periodic capture. - One analysis runs at a time. The default timeout is 15 seconds;
analysisTimeoutMsaccepts 1,000–60,000 ms. - Failure or an invalid receipt emits
analysis-error. There is no automatic retry or fallback to PEEPL AI; periodic captures continue independently. - Stopping proctoring aborts in-flight analysis. Custom callbacks should pass
the supplied
signalto their network requests. analysis-completemeans backend submission succeeded, not that incident processing or storage has finished.
External analysis mode comes from the authenticated iframe handshake, not a
browser setting. External face monitoring requires an analyzeCamera callback;
otherwise it fails with client-analysis-required.
Use a browser model for the preliminary check
The built-in YuNet detector provides live face monitoring and profile-photo guidance. It loads lazily and is released when the SDK is destroyed. Browser results are advisory; they do not replace backend incident analysis.
Choose file replacement or an adapter
| Change | Supported approach |
|---|---|
| Host the built-in model elsewhere | Set assetsUrl to the complete detector assets directory. |
| Use a compatible YuNet ONNX model | Replace the expected model file in your hosted assets. |
| Change model architecture or preprocessing, such as MediaPipe, SCRFD, or YOLO | Supply a faceDetector adapter. |
There is no modelUrl option. assetsUrl is a directory, not a single model
URL. The built-in engine requests models/face_detection_yunet_2026may.onnx
inside that directory. Renaming an incompatible model does not make it work.
Compatible replacements must accept float32 [1, 3, 640, 640], planar BGR pixels
without normalization, and top-left letterboxing. Outputs must match YuNet's
cls, obj, bbox, and kps tensors for strides 8, 16, and 32, including names
such as cls_8, shapes, layout, and decoding. The detector uses a score threshold
of 0.6 and NMS IoU of 0.3.
Self-host or replace a compatible model file
Build the SDK, then copy the contents of
packages/client/dist/ into your web server's public/proctoring/ directory.
These browser URLs must resolve:
/proctoring/index.iife.js
/proctoring/face-detector.iife.js
/proctoring/models/face_detection_yunet_2026may.onnx
/proctoring/wasm/ort-wasm-simd-threaded.mjs
/proctoring/wasm/ort-wasm-simd-threaded.wasmServe .mjs as JavaScript and .wasm as application/wasm, not an HTML fallback.
For assets on another origin, allow CORS from your page. Serve the page over
HTTPS or local loopback HTTP, not file://.
<iframe id="online-test" src="https://test.peepl.tech/" allow="camera; microphone"></iframe>
<script src="/proctoring/index.iife.js"></script>
<script>
const proctor = NucleusProctor.init({
iframe: '#online-test',
assetsUrl: '/proctoring/',
})
window.addEventListener('pagehide', () => proctor.destroy())
</script>For a CDN, use the full assets directory URL for assetsUrl, such as
https://cdn.client.example/proctoring/.
A compatible alternative is OpenCV Zoo's
face_detection_yunet_2023mar.onnx.
Run this command inside your hosted public/proctoring/ directory:
curl --fail --location \
'https://media.githubusercontent.com/media/opencv/opencv_zoo/main/models/face_detection_yunet/face_detection_yunet_2023mar.onnx' \
--output models/face_detection_yunet_2026may.onnxThe destination filename stays the same; its contents are the compatible 2023 model. The HTML integration is unchanged. Preserve the upstream model license.
Different architecture: MediaPipe adapter
This example uses MediaPipe BlazeFace instead of YuNet. Use it instead of
the previous initialization, with /proctoring/index.iife.js hosted as above.
<iframe id="online-test" src="https://test.peepl.tech/" allow="camera; microphone"></iframe>
<script src="/proctoring/index.iife.js"></script>
<script type="module">
import {
FaceDetector as MediaPipeDetector,
FilesetResolver,
} from 'https://cdn.jsdelivr.net/npm/@mediapipe/[email protected]/vision_bundle.mjs'
async function createMediaPipeDetector() {
const vision = await FilesetResolver.forVisionTasks(
'https://cdn.jsdelivr.net/npm/@mediapipe/[email protected]/wasm'
)
const detector = await MediaPipeDetector.createFromOptions(vision, {
baseOptions: {
modelAssetPath:
'https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/1/blaze_face_short_range.tflite',
delegate: 'CPU',
},
runningMode: 'IMAGE',
minDetectionConfidence: 0.6,
minSuppressionThreshold: 0.3,
})
let disposed = false
return {
async detect(input) {
if (disposed) throw new Error('face-detector-disposed')
return detector.detect(input).detections.map(({ boundingBox }) => {
if (!boundingBox) throw new Error('face-bounding-box-missing')
return {
x: boundingBox.originX,
y: boundingBox.originY,
width: boundingBox.width,
height: boundingBox.height,
}
})
},
dispose() {
if (disposed) return
disposed = true
detector.close()
},
}
}
const proctor = NucleusProctor.init({
iframe: '#online-test',
faceDetector: createMediaPipeDetector,
})
window.addEventListener('pagehide', () => proctor.destroy())
</script>IMAGE mode accepts both webcam video frames and profile-photo images. MediaPipe
handles its own preprocessing and assets; the built-in YuNet assets are not loaded.
Allow the CDN, model, and runtime URLs in your CSP, or host matching assets yourself.
Inference is synchronous and can block the UI thread.
For external backend analysis, configure analyzeCamera independently of either
browser model example.
Adapter contract
faceDetectoris an async factory returning{ detect, dispose }.detectaccepts anHTMLVideoElementorHTMLImageElementand returns a promise of face boxes in original-image pixels:[{ x, y, width, height }].- Return
[]for successful inference with no face; throw on detection failure. - The SDK loads the factory lazily, reuses the detector, and calls
disposewhen destroyed.
This browser contract needs only boxes. The backend contract also needs scores and five landmarks. MediaPipe Face Detector lacks separate mouth corners, so this example is not a backend-analysis adapter.
API
| Member | Description |
|---|---|
| init({ iframe, childOrigin?, assetsUrl?, faceDetector?, analyzeCamera?, analysisTimeoutMs? }) | Initialize once per page. iframe is a selector or element; childOrigin defaults to its src origin. |
| assetsUrl | Directory containing the built-in detector assets. |
| faceDetector | Lazy factory for a custom browser detector. |
| analyzeCamera | Async callback returning a submitted receipt in external mode. |
| createCameraAnalyzer(endpoint, { headers? }?) | Same-origin JSON endpoint adapter. |
| destroy() | Stop proctoring, cancel analysis, release media and models, and remove listeners. |
| status() | Current { state, sid, camera, screen, proctoring, cameraAnalysis }; analysis mode is internal or external. |
| on(event, handler) | Subscribe to SDK events. |
Events: state, handshake, stop, error, connection-lost, protocol,
analysis-error, analysis-complete.
Common errors: hello-timeout, camera-denied, screen-denied, heartbeat-dead,
and client-analysis-required.
