camera

Source: camera.js:22

Webcam lifecycle and the per-frame effect loop driver. Three areas of responsibility, kept in one module because they share the els.video element and the same lifecycle assumption ("the camera is started once and stays running"):

  1. Webcam acquisition: requesting getUserMedia, attaching the stream, applying the mirror transform based on facing mode, and resizing the overlay canvas to match the video's native dimensions.
  2. Effect render loop: throttled requestAnimationFrame driver that calls runEffectPass() from engine.js at the rate selected by the FPS dropdown.
  3. One-second recording: a small MediaRecorder wrapper used by the workshop's "capture and share" button.

No module-level mutable state lives here except effectLoopHandle (the rAF id used to cancel the loop on stop). All other state goes through state.js.


Static Methods

startCamera(): Promise.<void>

Acquire the webcam stream, attach it to the <video> element, configure mirroring based on the current facing mode, and start the per-frame effect loop. Resolves once the video has loaded its metadata and is actually playing — callers can treat resolution as "the live feed is visible on screen".

Failure modes:

  • navigator.mediaDevices missing (insecure context, e.g. HTTP on a LAN IP instead of localhost or HTTPS): logs an actionable message and throws.
  • getUserMedia rejection: propagates the underlying error.

Returns

  • Promise.<void>

Throws

  • Error — If the page is in an insecure context, or the user denies camera permission.
  • See:
    • scripts/main.js – called once at the end of `init()` after every model has been loaded.
    • startEffectLoop – kicked off here.

resizeCanvas()

Align the overlay canvas's intrinsic dimensions to the video's native resolution. CSS object-fit: cover then handles the visual crop to the container, which means coordinates returned by face-api / MediaPipe (in video-pixel space) project 1:1 onto the canvas without stretching, even on screen aspect ratios different from the camera's. Falls back to the container's bounding box before the video has known dimensions (during boot, before camera permissions are granted).

  • See:
    • startCamera – called once after the stream starts.
    • scripts/engine.js – `drawGhostyleOverlay()` calls this before drawing, in case the window has been resized since the last frame.

effectLoop(ts?: number): Promise.<void>

One iteration of the effect render loop. Runs runEffectPass() from engine.js if at least currentDelay milliseconds have passed since the last execution (so the FPS dropdown actually throttles the inference, even though requestAnimationFrame itself fires every 16–17 ms). Schedules the next iteration unconditionally — to stop the loop, call stopEffectLoop().

Parameters

  • ts (number, optional, default: 0) — Timestamp from rAF; defaults to 0 for the very first frame so the first runEffectPass runs immediately.

Returns

  • Promise.<void>
  • See:
    • runEffectPass – the actual face-api inference call.
    • startEffectLoop – schedules the first iteration.

startEffectLoop()

Start (or restart) the effect render loop. Cancels any previously scheduled rAF first so successive calls don't stack up parallel loops — useful when the camera is restarted after a facingMode switch.

  • See:
    • effectLoop – the function actually scheduled.
    • startCamera – calls this once the stream is live.

stopEffectLoop()

Stop the effect render loop and reset the inference-in-flight guard. Currently called only from unit tests; no production code path teardown the loop because the page lifecycle does it implicitly. Kept for completeness and as a test-only handle.

  • See:
    • startEffectLoop – the symmetric counterpart.
    • tests/unit/camera.test.js – asserts the cancel + null-out behaviour.

recordOneSecond(): Promise.<void>

Capture a short video clip from the live webcam stream and emit it as a browser-managed Blob for the upload/consent panel. Honours state.isRecording and state.isSystemBusy to refuse overlapping captures, and updates the record button visual state for the duration of the recording.

MIME-type selection is best-effort: prefers H.264 MP4 (broadest player support), falls back to MP4 generic, then VP9 WebM, then VP8 WebM. If none of these are supported, MediaRecorder will throw and the error is surfaced into the log.

Returns

  • Promise.<void>
  • See:
    • RECORDING_CONFIG – controls mode, endpoint, and duration.
    • scripts/main.js – wires the `recordBtn` click handler to this.

Static Fields

effectLoopHandle

Handle returned by the most recent requestAnimationFrame for the effect render loop. Exported so tests can inspect the start / stop transitions. null when the loop is not running.