Camera Photo (single-shot)
Contract-level feature. This page documents the
camera.photowire 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.photoin 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
maxWidthis 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.