Camera Capture and PTZ Control for Paired Nodes

Learn how to capture photos and videos from iOS, Android, macOS, and Linux nodes, and control physical PTZ on macOS cameras. Includes setup and command details.

Read this when

  • Adding or modifying camera capture on node platforms
  • Controlling a USB camera's physical pan, tilt, or zoom on macOS
  • Extending agent-accessible MEDIA temp-file workflows

OpenClaw lets paired iOS, Android, macOS, and Linux nodes capture camera media for agent workflows. Through Gateway node.invoke, you can grab a still image (jpg) or a brief video segment (mp4, with sound if desired).

On macOS, supported USB UVC cameras can be physically repositioned by the app, including pan, tilt, and zoom actions. PTZ alters the hardware's orientation; it never rotates, crops, or modifies the resulting image in any way.

Each platform controls camera access through a user-defined setting that must be enabled.

iOS node

iOS user setting

  • In the iOS Settings tab, navigate to Camera and enable Allow Camera (camera.enabled).
    • Default: on (a missing key counts as enabled).
    • When disabled: camera.* commands respond with CAMERA_DISABLED.

iOS commands (via Gateway node.invoke)

  • camera.list

    • Response payload: devices, which is an array of { id, name, position, deviceType }.
  • camera.snap

    • Params:
      • facing: front|back (defaults to front)
      • maxWidth: number, optional (default 1600)
      • quality: 0..1, optional (default 0.9, limited to [0.05, 1.0])
      • format: at this time jpg
      • delayMs: number, optional (default 0, with an internal ceiling of 10000)
      • deviceId: string, optional (chosen from camera.list)
    • Response payload: format: "jpg", base64, width, and height.
    • Payload guard: images get recompressed so the base64-encoded payload stays under 5MB.
  • camera.clip

    • Params:
      • facing: front|back (defaults to front)
      • durationMs: number (default 3000, restricted to [250, 60000])
      • includeAudio: boolean (default true)
      • format: at this time mp4
      • deviceId: string, optional (chosen from camera.list)
    • Response payload: format: "mp4", base64, durationMs, and hasAudio.

iOS foreground requirement

The iOS node accepts camera.* commands only when running in the foreground. Calls made in the background yield NODE_BACKGROUND_UNAVAILABLE.

CLI helper

For pulling media files, the CLI helper is the simplest route: it writes decoded media to a temporary file and outputs the path where it was saved.

openclaw nodes camera snap --node <id>                 # default: one node-selected photo
openclaw nodes camera snap --node <id> --facing front
openclaw nodes camera snap --node <id> --facing both   # front then back (2 saved paths)
openclaw nodes camera clip --node <id> --duration 3000
openclaw nodes camera clip --node <id> --no-audio

Without --facing, a single photo is taken by nodes camera snap through the node's default camera, and the resulting artifact gets the label unknown. On nodes that are not Linux, --facing both grabs the front camera first, then the back, and outputs two saved paths. --device-id works on its own without --facing; however, on non-Linux nodes, pairing it with --facing both is not allowed. Linux consistently sends a single request with no facing specified, tagging the artifact as unknown, no matter what --facing is set to. Output files are only temporary, living in the OS temp directory, unless you create a custom wrapper.

Android node

Android user setting

  • Android Settings sheet → CameraAllow Camera (camera.enabled).
    • Fresh installs default to off. Existing installs that predate this setting are migrated to on so upgrades do not silently lose previously working camera access.
    • When off: camera.* commands return CAMERA_DISABLED: enable Camera in Settings.

Permissions

  • Both camera.snap and camera.clip demand CAMERA; if the permission is absent or denied, CAMERA_PERMISSION_REQUIRED is returned.
  • When includeAudio equals true, camera.clip requires RECORD_AUDIO; a missing or denied permission results in MIC_PERMISSION_REQUIRED.

Runtime permission prompts are shown by the app whenever feasible.

Android foreground requirement

Only in the foreground does the Android node permit camera.* commands. If invoked in the background, NODE_BACKGROUND_UNAVAILABLE: command requires foreground is returned.

Android commands (via Gateway node.invoke)

  • camera.list

    • Response payload: devices, array of { id, name, position, deviceType }.
  • camera.snap

    • Params: facing (front|back, default front), quality (default 0.95, clamped to [0.1, 1.0]), maxWidth (default 1600), deviceId (optional; unknown id fails with INVALID_REQUEST).
    • Response payload: format: "jpg", base64, width, height.
    • Payload guard: recompressed to keep base64 under 5MB (same budget as iOS).
  • camera.clip

    • Params: facing (default front), durationMs (default 3000, limited to [200, 60000]), includeAudio (default true), deviceId (optional).
    • Response payload: format: "mp4", base64, durationMs, hasAudio.
    • Payload guard: raw MP4 is capped at 18MB before base64 encoding; oversized clips fail with PAYLOAD_TOO_LARGE (lower durationMs and try again).

macOS app

macOS user setting

The macOS companion app provides a checkbox:

  • Settings → General → Allow Camera (openclaw.cameraEnabled).
    • Default: off.
    • When off: camera requests return CAMERA_DISABLED: enable Camera in Settings.

CLI helper (node invoke)

Use the main openclaw CLI to run camera commands on the macOS node.

openclaw nodes camera list --node <id>                     # list camera ids
openclaw nodes camera snap --node <id>                     # prints saved path
openclaw nodes camera snap --node <id> --max-width 1280
openclaw nodes camera snap --node <id> --delay-ms 2000
openclaw nodes camera snap --node <id> --device-id <id>
openclaw nodes camera clip --node <id> --duration 10s       # prints saved path
openclaw nodes camera clip --node <id> --duration-ms 3000   # prints saved path (legacy flag)
openclaw nodes camera clip --node <id> --device-id <id>
openclaw nodes camera clip --node <id> --no-audio
  • openclaw nodes camera snap defaults to maxWidth=1600 unless overridden.
  • camera.snap waits delayMs (default 2000ms, limited to [0, 10000]) after warm-up/exposure settle before capturing.
  • Photo payloads are recompressed to keep base64 under 5MB.

macOS physical PTZ

Physical PTZ is handled by the Mac app for USB cameras with standard UVC absolute pan/tilt or zoom controls. It relies on the same Allow Camera setting as capture. Other node platforms do not advertise these commands.

Always pass an explicit deviceId returned by camera.list. OpenClaw never selects a default camera for physical movement.

  • camera.ptz.status is a safe read command. Request: { "deviceId": "<camera-id>" }.
    • The response contains only executable pan, tilt, and zoom axes under axes.
    • Pan and tilt values are degrees. Zoom values are percentages.
    • Each axis reports current, min, max, step, unit, canSet, and canMove. default appears only when the camera successfully reports a device default.
    • canHome is true only when every executable exposed axis has a real device-advertised default, so the complete home plan can be attempted.
  • camera.ptz.control changes the camera hardware. Its closed operations are:
    • { "deviceId": "<camera-id>", "operation": "set", "target": { "panDegrees": 10, "tiltDegrees": -5, "zoomPercent": 40 } }
    • { "deviceId": "<camera-id>", "operation": "move", "delta": { "panDegrees": 2, "zoomPercent": -5 } }
    • { "deviceId": "<camera-id>", "operation": "home" }

set and move require at least one finite axis value. Omitted axes remain unchanged, and move deltas for zoom are percentage points. home restores the device-advertised defaults; it returns CAMERA_PTZ_UNSUPPORTED without moving the camera when canHome is false. The Mac app clamps and snaps requested values to the camera's range and resolution; the response returns the post-operation state and lists changed request fields in adjusted. Requesting an unsupported axis returns CAMERA_PTZ_AXIS_UNSUPPORTED.

Pan/tilt and zoom use separate hardware writes and cannot be atomic. If an earlier control group succeeds but a later write or final status read fails, CAMERA_PTZ_PARTIAL names the applied groups, includes best-effort resulting state when readable, and tells the caller to run camera.ptz.status before retrying.

camera.ptz.control is unsafe and stays inactive until the operator manually adds it to gateway.nodes.commands.allow:

{
  gateway: {
    nodes: {
      commands: { allow: ["camera.ptz.control"] },
    },
  },
}

The allow entry by itself does not expand an existing node approval. Once the updated Mac reconnects and announces PTZ control, execute openclaw nodes pending, then use openclaw nodes approve <requestId> to approve the extended surface.

Within the agent nodes tool, supply action: "camera_ptz", the chosen Mac node, deviceId, and ptzOperation: "status" | "set" | "move" | "home". The axis parameters are panDegrees, tiltDegrees, and zoomPercent.

Linux node host

The bundled Linux Node plugin introduces camera capture to the CLI openclaw node service. It operates on a headless host and does not need the Linux desktop application.

Camera access starts disabled. Turn it on through the plugin entry, then restart the node service so its Gateway advertisement gets regenerated:

{
  plugins: {
    entries: {
      "linux-node": {
        config: {
          camera: { enabled: true },
        },
      },
    },
  },
}

Prerequisites:

  • FFmpeg compiled with V4L2 input, libx264, and AAC support
  • a /dev/video* device that the node-service user can read; on typical distributions, add that user to the video group
  • for clips using the default includeAudio: true, a functional PulseAudio server or PipeWire PulseAudio compatibility layer with a default source

Linux returns readable, capture-capable V4L2 device paths from camera.list; FFmpeg checks each /dev/video* candidate and filters out metadata-only or output-only nodes. Device position is unknown, so facing requests without deviceId yield one unknown-position photo or clip rather than claiming a front or back camera. Use deviceId when a host has multiple cameras. camera.snap relies on FFmpeg input warm-up for delayMs and maintains aspect ratio while capping width. camera.clip captures microphone audio as the MP4 audio track; OpenClaw intentionally exposes no standalone microphone command.

The plugin uses libx264 for MP4 video and never silently changes codecs. An FFmpeg build missing the required input or encoders returns CAMERA_UNAVAILABLE. Photos and clips that go beyond the 25MB base64 payload budget fail with PAYLOAD_TOO_LARGE.

camera.snap and camera.clip are still dangerous commands. Add them to gateway.nodes.commands.allow only when you plan to arm capture; enabling the plugin alone does not override Gateway policy.

Safety + practical limits

  • Camera and microphone access trigger the standard OS permission prompts (and require usage strings in Info.plist).
  • Video clips are limited to 60s to prevent oversized node payloads (base64 overhead plus message limits).

macOS screen video (OS-level)

For screen video (not camera), use the macOS companion:

openclaw nodes screen record --node <id> --duration 10s --fps 15   # prints saved path

Requires macOS Screen Recording permission (TCC).

1,648 words · updated Aug 25, 2026