Camera Photo (single-shot)

Contract-level feature. This page documents the camera.photo wire contract and its SDK projection as specified in the Runtime v1 bridge contract (platform 0.7). It is deliberately narrow; read the security constraints before designing a feature around it.

The baseline BodyLink rule is that camera pixels never cross into the game iframe — the host composites its own preview, and your game gets landmarks, not video. camera.photo is the one sanctioned exception: a capability-gated, single-shot, host-captured still photo delivered into the game as an encoded JPEG.

No stream, no texture, no continuous frames — one request, at most one still, and the host may deny any request.


Declaring the capability

The channel is closed by default. Declare camera.photo in your manifest capabilities, or the request is refused before any pixel is touched:

// bodylink.game.json
        {
          "capabilities": ["iframe.bundle.v1", "sdk.proxy.v1", "tracking.frame", "camera.photo"]
        }
        

The wire shape

Two message kinds on the iframe bridge, correlated by correlationId:

Kind Direction Payload
camera.photo.capture game → host { maxWidth?: number } — requested photo width in pixels. Default 640; the host clamps to [16, 1280] (a game can never demand a full-resolution sensor frame).
camera.photo.result host → game { ok: true, photo, width, height, mirrored: true, tsMs } on success, or { ok: false, code: 'capability_required' | 'no-camera' | 'capture-failed' }.

On success, photo is a data:image/jpeg;… data URL: the host captures one frame from its own live camera source, full frame at the capture aspect, mirrored like the on-screen preview (mirrored: true always), JPEG at quality ≈ 0.85, downscaled to the clamped maxWidth. No drawable camera source ⇒ no-camera; any capture/encode failure ⇒ capture-failed. The host never throws across the bridge.

The SDK projection

game.cameraPhoto.capture(options) wraps the request/response pair. It always resolves and never rejects or hangs:

const result = await game.cameraPhoto.capture({ maxWidth: 640 });

        if (result.ok) {
          photoImg.src = result.photo;          // data:image/jpeg;…
          // result.width / result.height / result.mirrored / result.tsMs
        } else {
          switch (result.code) {
            case 'unsupported':          // standalone session, or a host that predates the kind
            case 'capability_required':  // manifest never declared camera.photo
            case 'no-camera':            // host has no drawable camera source
            case 'capture-failed':       // capture/encode failed, or the host denied
              hidePhotoFeature();
          }
        }
        

Design for denial: every failure mode arrives as a coded { ok: false }, so a photo feature must degrade gracefully — treat the photo as a bonus, never a gameplay dependency.

Security constraints (normative)

  • Capability-gated, mirroring the preview slot channel: without camera.photo in the manifest and the negotiated session capabilities, the request is refused and no pixels ever cross.
  • Single-shot per request. Another photo means another request — and the host may deny any of them. There is no subscription form.
  • Bounded resolution. The untrusted maxWidth is clamped host-side (default 640, max 1280).
  • Validated egress. The bridge validates the host's result before it crosses (a well-formed image data URL with positive dimensions, or a coded denial); anything malformed degrades to capture-failed.

Everything else about the camera stays behind the boundary — see Camera Preview & Coordinates for the pixel-free preview channel.