Apus supports camera capture for agent workflows on paired iOS, Android, macOS, and Linux nodes: capture a photo (jpg) or a short video clip (mp4, with optional audio) via Gateway node.invoke.

When a capture request includes deviceId, the selected camera must match that ID exactly. An unknown ID fails instead of capturing from a different camera; run camera.list to refresh device IDs, which change when cameras are reconnected.

The macOS app can also physically pan, tilt, and zoom supported USB UVC cameras. PTZ moves the camera hardware; it does not rotate, crop, or otherwise transform a captured image.

All camera access is gated behind a user-controlled setting per platform.

iOS node

iOS user setting

iOS commands (via Gateway node.invoke)

iOS foreground requirement

The iOS node only allows camera.* commands in the foreground. Background invocations return NODE_BACKGROUND_UNAVAILABLE.

CLI helper

The easiest way to get media files is via the CLI helper, which writes decoded media to a temp file and prints the saved path.

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

--duration accepts a bare number of milliseconds (3000) or a unit suffix (10s, 1m, 1h30m). It defaults to 3000 (3 seconds).

Without --facing, nodes camera snap captures one photo using the node's default camera and labels the saved artifact unknown. On non-Linux nodes, --facing both captures front then back and prints two saved paths. --device-id is valid without --facing; on non-Linux nodes, it cannot be combined with --facing both. Linux always sends one facing-less request and labels the artifact unknown, regardless of --facing. Output files are temporary (in the OS temp directory) unless you build your own wrapper.

Android node

Android user setting

Permissions

The app prompts for runtime permissions when possible.

Android foreground requirement

The Android node only allows camera.* commands in the foreground. Background invocations return NODE_BACKGROUND_UNAVAILABLE: command requires foreground.

Android commands (via Gateway node.invoke)

macOS app

macOS user setting

The macOS companion app exposes a checkbox:

CLI helper (node invoke)

Use the main apus CLI to invoke camera commands on the macOS node.

apus nodes camera list --node <id>                     # list camera ids
apus nodes camera snap --node <id>                     # prints saved path
apus nodes camera snap --node <id> --max-width 1280
apus nodes camera snap --node <id> --delay-ms 2000
apus nodes camera snap --node <id> --device-id <id>
apus nodes camera clip --node <id> --duration 10s       # prints saved path
apus nodes camera clip --node <id> --device-id <id>
apus nodes camera clip --node <id> --no-audio

If a macOS external camera starts a photo session in portrait, Apus selects an advertised landscape format with transposed dimensions and the same encoding, when one exists. Already-landscape, built-in, and Continuity Camera formats are unchanged. Without an exact counterpart, or if the camera cannot be reconfigured, capture keeps the negotiated format. --max-width still only limits the returned JPEG width; it does not choose a camera format.

macOS physical PTZ

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

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

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.

Both PTZ commands briefly open a live camera stream because supported cameras only service UVC controls while streaming. This activates the camera and its privacy indicator for the duration, including when camera.ptz.status only reads the position. Frames are not retained, and no photo, video, or file is produced.

Pan/tilt and zoom use separate hardware writes and cannot be atomic. Apus verifies the resulting position through a fresh control connection. If a later write or final status read fails, or an axis does not reach its requested position within the camera's reported resolution, CAMERA_PTZ_PARTIAL names the acknowledged control groups, includes the independently observed state when readable, and tells the caller to run camera.ptz.status before retrying. Position failures also report the requested and observed values; check that a video stream reaches the camera and disable on-camera AI framing or tracking that can override UVC controls.

camera.ptz.control is dangerous and remains disarmed until the operator explicitly adds it to gateway.nodes.commands.allow:

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

The allow entry alone does not widen an existing node approval. After the updated Mac reconnects and declares PTZ control, run apus nodes pending, then approve the widened surface with apus nodes approve <requestId>.

In the agent nodes tool, use action: "camera_ptz", the selected Mac node, deviceId, and ptzOperation: "status" | "set" | "move" | "home". Axis inputs are panDegrees, tiltDegrees, and zoomPercent.

Linux node host

The bundled Linux Node plugin adds camera capture to the CLI apus node service. It works on a headless host and does not require the Linux desktop app.

Camera access defaults to off. Enable it under the plugin entry, then restart the node service so its Gateway advertisement is rebuilt:

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

Requirements:

Linux returns capture-capable, readable V4L2 device paths from camera.list; FFmpeg checks each /dev/video* candidate and omits metadata or output-only nodes. Device position is unknown, so facing requests without deviceId produce one unknown-position photo or clip instead of claiming a front or back camera. Use deviceId when a host has multiple cameras. camera.snap uses FFmpeg input warm-up for delayMs and preserves aspect ratio while limiting width. camera.clip records microphone audio as the MP4 audio track; Apus deliberately exposes no standalone microphone command.

The plugin uses libx264 for MP4 video and does not silently change codecs. An FFmpeg build without the required input or encoders returns CAMERA_UNAVAILABLE. Photos and clips that would exceed the 25MB base64 payload budget fail with PAYLOAD_TOO_LARGE.

camera.snap and camera.clip remain dangerous commands. Add them to gateway.nodes.commands.allow only when you intend to arm capture; enabling the plugin alone does not bypass Gateway policy.

Safety + practical limits

macOS screen video (OS-level)

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

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

Requires macOS Screen Recording permission (TCC).

Related