---
title: "cameraInput"
description: "Physical cameras as live textures and pixel sources: list the capture devices (a webcam, a phone's front and back cameras), open one as a feed…"
section: "API Reference"
slug: "api-camerainput"
canonical: "https://origozero.ai/docs/api-camerainput"
updated: "2026-09-27T11:06:28.146412727+00:00"
tags: ["api", "reference"]
---

# cameraInput

Physical cameras as live textures and pixel sources: list the capture devices (a webcam, a phone's front and back cameras), open one as a feed, show its live picture anywhere a texture goes (a material, a UI image, a recording), and read its pixels.

```lua
require("@builtin::modules.api.engine.cameraInput") -- cameraInput (also available as global 'cameraInput')
```

The `cameraInput` namespace: 19 functions.

## globals/cameraInput/devices {#globals-camerainput-devices}

```lua
cameraInput.devices() -> { CameraDevice }
```

Every capture device the platform offers: `id` is what
`cameraInput.open` takes as `device`, `name` the label a person
recognises, `facing` which way it points. Until camera access has been
granted, `id` and `name` read as empty strings and `facing` as
`"unknown"`: the list is part of what the permission protects, so a
first open takes the default camera or a facing, and the list names the
devices after that.

**Returns** `{ CameraDevice }` — Array of `{ id, name, facing, default }`.

```lua
for _, d in ipairs(cameraInput.devices()) do print(d.name, d.facing) end
```

## globals/cameraInput/feeds {#globals-camerainput-feeds}

```lua
cameraInput.feeds() -> { CameraFeedEntry }
```

Every open feed, with the owning context that opened it (`scope`,
the same tag `scopes.list()` shows), whose end stops the feed.

**Returns** `{ CameraFeedEntry }` — Array of `{ id, scope, state, device, texture }`.

```lua
for _, f in ipairs(cameraInput.feeds()) do print(f.id, f.state, f.scope) end
```

## globals/cameraInput/onDeviceChange {#globals-camerainput-ondevicechange}

```lua
cameraInput.onDeviceChange(callback: (devices: { CameraDevice }) -> ()) -> () -> ()
```

Call `callback` with the new device list every time a camera is
added or removed, or the platform names its devices after access is
granted. Each listener is watched by a task of the context that
registered it, so a listener a component registered ends when that
component is removed, and every other listener goes on hearing changes.

**Parameters**

- `callback` `(devices: { CameraDevice }) -> ()` — Receives the device list, as `cameraInput.devices()` returns it.

**Returns** `() -> ()` — A function that removes this listener.

```lua
local off = cameraInput.onDeviceChange(function(devices) print(#devices) end)
```

## globals/cameraInput/open {#globals-camerainput-open}

```lua
cameraInput.open(opts: CameraOpenOpts?) -> (CameraFeed?, string?)
```

Open a camera as a feed. Returns the feed at once; it reads
`permissionPending` while the platform asks for access, then `running`
(or `denied` / `failed` with a reason). Wait with `feed:awaitRunning()`.

`feed.texture` is a texture handle that shows the camera's current frame:
put it on a surface as a session material's `base_color_texture`
(`renderer.material.create`, worn with `Model:applySessionMaterial`) or in
a slot of a material that exists (`renderer.material.setTexture(key,
"base_color_texture", feed.texture)`), in UI as
`{ type = "image", props = { src = feed.texture } }`,
and `av.record` films it like any other scene content.

`device` names a camera by the id or name `cameraInput.devices()` lists;
`facing` asks for the `"front"` or `"back"` camera instead; with neither,
the platform's default camera opens. `width`, `height` and `fps` are hints:
the device delivers the nearest it can, and `feed:status()` reports what it
delivers. A second feed on a camera already open shares it.

Returns nil and the reason for a request refused before any camera is
touched: a hint outside its range, a facing other than front or back, a
device the platform does not offer.

**Parameters**

- `opts` `CameraOpenOpts` _(optional)_ — `{ device, facing, width, height, fps }`, every field optional.

**Returns** `(CameraFeed?, string?)` — The feed, or nil and the reason.

```lua
local feed = cameraInput.open({ facing = "front", width = 1280, height = 720 })
local screen = feed and renderer.material.create({ shader = "unlit", textures = { base_color_texture = feed.texture } }, "tv_screen")
```

## globals/cameraInput/stopAll {#globals-camerainput-stopall}

```lua
cameraInput.stopAll() -> number
```

Stop every open feed and release every camera, whatever opened them.

**Returns** `number` — How many feeds stopped.

```lua
cameraInput.stopAll()
```

## modules/cameraInput/awaitRunning {#modules-camerainput-awaitrunning}

```lua
awaitRunning(self: CameraFeed, timeoutSeconds: number?): (boolean, string?)
```

Wait until the feed is running, and report whether it got there. Returns
as soon as the feed leaves `starting` and `permissionPending`, or when
`timeoutSeconds` have passed, whichever comes first: a permission prompt
nobody answers never settles, so the wait is always bounded.

**Parameters**

- `self` `CameraFeed`
- `timeoutSeconds` `number?` _(optional)_ — Seconds to wait at most. Defaults to 10.

```lua
local ok, why = feed:awaitRunning(15); if not ok then print(why) end
```

## modules/cameraInput/devices {#modules-camerainput-devices}

```lua
devices(): { CameraDevice }
```

Every capture device the platform offers: `id` is what
`cameraInput.open` takes as `device`, `name` the label a person
recognises, `facing` which way it points. Until camera access has been
granted, `id` and `name` read as empty strings and `facing` as
`"unknown"`: the list is part of what the permission protects, so a
first open takes the default camera or a facing, and the list names the
devices after that.

```lua
for _, d in ipairs(cameraInput.devices()) do print(d.name, d.facing) end
```

## modules/cameraInput/feeds {#modules-camerainput-feeds}

```lua
feeds(): { CameraFeedEntry }
```

Every open feed, with the owning context that opened it (`scope`,
the same tag `scopes.list()` shows), whose end stops the feed.

```lua
for _, f in ipairs(cameraInput.feeds()) do print(f.id, f.state, f.scope) end
```

## modules/cameraInput/onDeviceChange {#modules-camerainput-ondevicechange}

```lua
onDeviceChange(callback: (devices: { CameraDevice }) -> ()): () -> ()
```

Call `callback` with the new device list every time a camera is
added or removed, or the platform names its devices after access is
granted. Each listener is watched by a task of the context that
registered it, so a listener a component registered ends when that
component is removed, and every other listener goes on hearing changes.

**Parameters**

- `callback` `(devices: { CameraDevice }) -> ()` — Receives the device list, as `cameraInput.devices()` returns it.

**Returns** `()` — A function that removes this listener.

```lua
local off = cameraInput.onDeviceChange(function(devices) print(#devices) end)
```

## modules/cameraInput/open {#modules-camerainput-open}

```lua
open(opts: CameraOpenOpts?): (CameraFeed?, string?)
```

Open a camera as a feed. Returns the feed at once; it reads
`permissionPending` while the platform asks for access, then `running`
(or `denied` / `failed` with a reason). Wait with `feed:awaitRunning()`.

`feed.texture` is a texture handle that shows the camera's current frame:
put it on a surface as a session material's `base_color_texture`
(`renderer.material.create`, worn with `Model:applySessionMaterial`) or in
a slot of a material that exists (`renderer.material.setTexture(key,
"base_color_texture", feed.texture)`), in UI as
`{ type = "image", props = { src = feed.texture } }`,
and `av.record` films it like any other scene content.

`device` names a camera by the id or name `cameraInput.devices()` lists;
`facing` asks for the `"front"` or `"back"` camera instead; with neither,
the platform's default camera opens. `width`, `height` and `fps` are hints:
the device delivers the nearest it can, and `feed:status()` reports what it
delivers. A second feed on a camera already open shares it.

Returns nil and the reason for a request refused before any camera is
touched: a hint outside its range, a facing other than front or back, a
device the platform does not offer.

**Parameters**

- `opts` `CameraOpenOpts?` _(optional)_ — `{ device, facing, width, height, fps }`, every field optional.

```lua
local feed = cameraInput.open({ facing = "front", width = 1280, height = 720 })
local screen = feed and renderer.material.create({ shader = "unlit", textures = { base_color_texture = feed.texture } }, "tv_screen")
```

## modules/cameraInput/readPixels {#modules-camerainput-readpixels}

```lua
readPixels(self: CameraFeed, rect: CameraRect?): (any, string?)
```

Read the pixels of the frame the feed is showing now, without stopping
it: the whole frame, or the rectangle `{ x, y, w, h }` of it in pixels from
the top-left corner. The answer is a `TextureCpuHandle` holding RGBA8
pixels (`:readPixel(x, y)`, `:rgba()`, `:encodePng()`); `:unload()` it once
done. Returns nil and the reason while no frame has arrived, after the feed
stopped, or for a rectangle outside the frame.

**Parameters**

- `self` `CameraFeed`
- `rect` `CameraRect?` _(optional)_ — Optional `{ x, y, w, h }`.

```lua
local cpu = feed:readPixels(); if cpu then print(cpu:readPixel(0, 0)); cpu:unload() end
```

## modules/cameraInput/status {#modules-camerainput-status}

```lua
status(self: CameraFeed): CameraStatus
```

Where the feed stands. `width`, `height` and `fps` are what the device
delivers, whatever the open asked for. `frames` advances once per new
frame, so two reads that show the same count saw the same picture.
`rotation` is the degrees clockwise the picture turns to stand upright on
the display, and follows a phone's screen as it turns; `mirrored` says the
picture, once turned, reads as the person expects only when shown
mirrored, which is how a front camera is shown. `reason` carries the
platform's own message for `denied` and `failed`, and what is being waited
on for `permissionPending`.

**Parameters**

- `self` `CameraFeed`

```lua
local s = feed:status(); print(s.state, s.width, s.height, s.frames)
```

## modules/cameraInput/stop {#modules-camerainput-stop}

```lua
stop(self: CameraFeed): boolean
```

Stop the feed. The camera is released once no other feed is reading
it, which is what turns the platform's camera indicator off. True when the
feed was open.

**Parameters**

- `self` `CameraFeed`

```lua
feed:stop()
```

## modules/cameraInput/stopAll {#modules-camerainput-stopall}

```lua
stopAll(): number
```

Stop every open feed and release every camera, whatever opened them.

```lua
cameraInput.stopAll()
```

## typed/builtin//modules/api/engine/cameraInput/cameraInput/devices {#typed-builtin-modules-api-engine-camerainput-camerainput-devices}

```lua
cameraInput.devices() -> { CameraDevice }
```

Every capture device the platform offers: `id` is what
`cameraInput.open` takes as `device`, `name` the label a person
recognises, `facing` which way it points. Until camera access has been
granted, `id` and `name` read as empty strings and `facing` as
`"unknown"`: the list is part of what the permission protects, so a
first open takes the default camera or a facing, and the list names the
devices after that.

**Returns** `{ CameraDevice }` — Array of `{ id, name, facing, default }`.

```lua
for _, d in ipairs(cameraInput.devices()) do print(d.name, d.facing) end
```

## typed/builtin//modules/api/engine/cameraInput/cameraInput/feeds {#typed-builtin-modules-api-engine-camerainput-camerainput-feeds}

```lua
cameraInput.feeds() -> { CameraFeedEntry }
```

Every open feed, with the owning context that opened it (`scope`,
the same tag `scopes.list()` shows), whose end stops the feed.

**Returns** `{ CameraFeedEntry }` — Array of `{ id, scope, state, device, texture }`.

```lua
for _, f in ipairs(cameraInput.feeds()) do print(f.id, f.state, f.scope) end
```

## typed/builtin//modules/api/engine/cameraInput/cameraInput/onDeviceChange {#typed-builtin-modules-api-engine-camerainput-camerainput-ondevicechange}

```lua
cameraInput.onDeviceChange(callback: (devices: { CameraDevice }) -> ()) -> () -> ()
```

Call `callback` with the new device list every time a camera is
added or removed, or the platform names its devices after access is
granted. Each listener is watched by a task of the context that
registered it, so a listener a component registered ends when that
component is removed, and every other listener goes on hearing changes.

**Parameters**

- `callback` `(devices: { CameraDevice }) -> ()` — Receives the device list, as `cameraInput.devices()` returns it.

**Returns** `() -> ()` — A function that removes this listener.

```lua
local off = cameraInput.onDeviceChange(function(devices) print(#devices) end)
```

## typed/builtin//modules/api/engine/cameraInput/cameraInput/open {#typed-builtin-modules-api-engine-camerainput-camerainput-open}

```lua
cameraInput.open(opts: CameraOpenOpts?) -> (CameraFeed?, string?)
```

Open a camera as a feed. Returns the feed at once; it reads
`permissionPending` while the platform asks for access, then `running`
(or `denied` / `failed` with a reason). Wait with `feed:awaitRunning()`.

`feed.texture` is a texture handle that shows the camera's current frame:
put it on a surface as a session material's `base_color_texture`
(`renderer.material.create`, worn with `Model:applySessionMaterial`) or in
a slot of a material that exists (`renderer.material.setTexture(key,
"base_color_texture", feed.texture)`), in UI as
`{ type = "image", props = { src = feed.texture } }`,
and `av.record` films it like any other scene content.

`device` names a camera by the id or name `cameraInput.devices()` lists;
`facing` asks for the `"front"` or `"back"` camera instead; with neither,
the platform's default camera opens. `width`, `height` and `fps` are hints:
the device delivers the nearest it can, and `feed:status()` reports what it
delivers. A second feed on a camera already open shares it.

Returns nil and the reason for a request refused before any camera is
touched: a hint outside its range, a facing other than front or back, a
device the platform does not offer.

**Parameters**

- `opts` `CameraOpenOpts` _(optional)_ — `{ device, facing, width, height, fps }`, every field optional.

**Returns** `(CameraFeed?, string?)` — The feed, or nil and the reason.

```lua
local feed = cameraInput.open({ facing = "front", width = 1280, height = 720 })
local screen = feed and renderer.material.create({ shader = "unlit", textures = { base_color_texture = feed.texture } }, "tv_screen")
```

## typed/builtin//modules/api/engine/cameraInput/cameraInput/stopAll {#typed-builtin-modules-api-engine-camerainput-camerainput-stopall}

```lua
cameraInput.stopAll() -> number
```

Stop every open feed and release every camera, whatever opened them.

**Returns** `number` — How many feeds stopped.

```lua
cameraInput.stopAll()
```
